1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-12-01 14:32:54 +01:00

Merge remote-tracking branch 'origin/master' into background-javascript

# Conflicts:
#	app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java
This commit is contained in:
Daniele Gobbetti 2017-01-28 17:43:34 +01:00
commit f66f765fb6
94 changed files with 3156 additions and 1154 deletions

View File

@ -1,8 +1,10 @@
####Your issue is:
#### Your issue is:
*In case of a bug, do not forget to attach logs!*
####Your wearable device is:
#### Your wearable device is:
*Please specify model and firmware version if possible*
####Your android version is:
#### Your android version is:
#### Your Gadgetbridge version is:

View File

@ -1,5 +1,39 @@
###Changelog
####Version 0.17.3
* HPlus: Improve display of new messages and phone calls
* HPlus: Fix bug related to steps and heart rate
* Pebble: Support dynamic keys for natively supported watchfaces and watchapps (more stability accross versions)
* Pebble: Fix error Toast being displayed when TimeStyle watchface is not installed
* Mi Band 1+2: Support for connecting wihout BT pairing (workaround for certain connection problems)
####Version 0.17.2
* Pebble: Fix temperature unit in Timestyle Pebble watchface
* Add optional Cyrillic transliteration (for devices lacking the font)
####Version 0.17.1
* Pebble: Fix installation of some watchapps
* Pebble: Try to improve PebbleKit compatibility
* HPlus: Fix bug setting current date
####Version 0.17.0
* Add weather support through "Weather Notification" app
* Various fixes for K9 mail when using the generic notification receiver
* Add a preference to hide the persistent notification icon of Gadgetbridge
* Pebble: Support for build-in weather system app (FW 4.x)
* Pebble: Add weather support for various watchfaces
* Pebble: Add option to disable call display
* Pebble: Add option to automatically delete notifications that got dismissed on the phone
* Pebble: Bugfix for some PebbleKit enabled 3rd party apps (TCW and maybe other)
* Pebble 2/LE: Improve reliablitly and transfer speed
* HPlus: Improved discovery and pairing
* HPlus: Improved notifications (display + vibration)
* HPlus: Synchronize time and date
* HPlus: Display firmware version and battery charge
* HPlus: Near real time Heart rate measurement
* HPlus: Experimental synchronization of activity data (only sleep, steps and intensity)
* HPlus: Fix some disconnection issues
####Version 0.16.0
* New devices: HPlus (e.g. Zeblaze ZeBand), contributed by João Paulo Barraca
* ZeBand: Initial support: notifications, heart rate, sleep monitoring, user configuration, date+time

View File

@ -60,6 +60,7 @@ public class GBDaoGenerator {
addPebbleHealthActivityKindOverlay(schema, user, device);
addPebbleMisfitActivitySample(schema, user, device);
addPebbleMorpheuzActivitySample(schema, user, device);
addHPlusHealthActivityKindOverlay(schema, user, device);
addHPlusHealthActivitySample(schema, user, device);
new DaoGenerator().generateAll(schema, "app/src/main/java");
@ -224,17 +225,34 @@ public class GBDaoGenerator {
private static Entity addHPlusHealthActivitySample(Schema schema, Entity user, Entity device) {
Entity activitySample = addEntity(schema, "HPlusHealthActivitySample");
activitySample.implementsSerializable();
addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device);
activitySample.addByteArrayProperty("rawHPlusHealthData");
activitySample.addIntProperty("rawHPlusCalories").notNull();
activitySample.addIntProperty("rawHPlusDistance").notNull();
activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().primaryKey();
activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE);
activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE);
activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE);
addHeartRateProperties(activitySample);
activitySample.addIntProperty("distance");
activitySample.addIntProperty("calories");
return activitySample;
}
private static Entity addHPlusHealthActivityKindOverlay(Schema schema, Entity user, Entity device) {
Entity activityOverlay = addEntity(schema, "HPlusHealthActivityOverlay");
activityOverlay.addIntProperty(TIMESTAMP_FROM).notNull().primaryKey();
activityOverlay.addIntProperty(TIMESTAMP_TO).notNull().primaryKey();
activityOverlay.addIntProperty(SAMPLE_RAW_KIND).notNull().primaryKey();
Property deviceId = activityOverlay.addLongProperty("deviceId").primaryKey().notNull().getProperty();
activityOverlay.addToOne(device, deviceId);
Property userId = activityOverlay.addLongProperty("userId").notNull().getProperty();
activityOverlay.addToOne(user, userId);
activityOverlay.addByteArrayProperty("rawHPlusHealthData");
return activityOverlay;
}
private static void addCommonActivitySampleProperties(String superClass, Entity activitySample, Entity user, Entity device) {
activitySample.setSuperclass(superClass);
activitySample.addImport(MAIN_PACKAGE + ".devices.SampleProvider");

View File

@ -108,26 +108,33 @@ For more information read [this wiki article](https://github.com/Freeyourgadget/
3. Tap the Mi Band item to connect if you're not connected yet
4. To test, chose "Debug" from the menu and play around
Known Issues:
**Known Issues:**
* The initial connection to a Mi Band sometimes takes a little patience. Try to connect a few times, wait,
and try connecting again. This only happens until you have "bonded" with the Mi Band, i.e. until it
knows your MAC address. This behavior may also only occur with older firmware versions.
* If you use other apps like Mi Fit, and "bonding" with Gadgetbridge does not work, please
try to unpair the band in the other app and try again with Gadgetbridge.
* While all Mi Band devices are supported, some firmware versions might work better than others.
You can consult the [projects wiki pages](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Mi-Band)
to check if your firmware version is fully supported or if an upgrade/downgrade might be beneficial.
## Features (Liveview)
* set time (automatically upon connection)
* display notifications and vibrate
## Authors (in order of first code contribution)
## Authors
### Core Team (in order of first code contribution)
* Andreas Shimokawa
* Carsten Pfeiffer
* Daniele Gobbetti
### Additional device support
* João Paulo Barraca (HPlus)
## Contribute
Contributions are welcome, be it feedback, bugreports, documentation, translation, research or code. Feel free to work

View File

@ -26,8 +26,8 @@ android {
targetSdkVersion 23
// note: always bump BOTH versionCode and versionName!
versionName "0.16.0"
versionCode 80
versionName "0.17.3"
versionCode 84
}
buildTypes {
release {

View File

@ -220,8 +220,7 @@
</intent-filter>
</receiver>
<activity android:name=".externalevents.WeatherNotificationConfig"
android:label="mockup">
<activity android:name=".externalevents.WeatherNotificationConfig">
<intent-filter>
<action android:name="ru.gelin.android.weather.notification.ACTION_WEATHER_SKIN_PREFERENCES"/>
</intent-filter>

View File

@ -171,6 +171,10 @@ public class GBApplication extends Application {
return prefs.getBoolean("log_to_file", false);
}
public static boolean minimizeNotification() {
return prefs.getBoolean("minimize_priority", false);
}
static void setupDatabase(Context context) {
DBOpenHelper helper = new DBOpenHelper(context, DATABASE_NAME, null);
SQLiteDatabase db = helper.getWritableDatabase();

View File

@ -1,15 +1,19 @@
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.format.DateFormat;
import android.view.MenuItem;
import android.view.View;
import android.widget.CheckBox;
import android.widget.TextView;
import android.widget.TimePicker;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
public class AlarmDetails extends GBActivity {
@ -23,16 +27,19 @@ public class AlarmDetails extends GBActivity {
private CheckBox cbFriday;
private CheckBox cbSaturday;
private CheckBox cbSunday;
private GBDevice device;
private TextView smartAlarmLabel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_alarm_details);
Parcelable p = getIntent().getExtras().getParcelable("alarm");
alarm = (GBAlarm) p;
alarm = getIntent().getParcelableExtra("alarm");
device = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
timePicker = (TimePicker) findViewById(R.id.alarm_time_picker);
smartAlarmLabel = (TextView) findViewById(R.id.alarm_label_smart_wakeup);
cbSmartWakeup = (CheckBox) findViewById(R.id.alarm_cb_smart_wakeup);
cbMonday = (CheckBox) findViewById(R.id.alarm_cb_mon);
cbTuesday = (CheckBox) findViewById(R.id.alarm_cb_tue);
@ -47,6 +54,9 @@ public class AlarmDetails extends GBActivity {
timePicker.setCurrentMinute(alarm.getMinute());
cbSmartWakeup.setChecked(alarm.isSmartWakeup());
int smartAlarmVisibility = supportsSmartWakeup() ? View.VISIBLE : View.GONE;
cbSmartWakeup.setVisibility(smartAlarmVisibility);
smartAlarmLabel.setVisibility(smartAlarmVisibility);
cbMonday.setChecked(alarm.getRepetition(GBAlarm.ALARM_MON));
cbTuesday.setChecked(alarm.getRepetition(GBAlarm.ALARM_TUE));
@ -58,6 +68,14 @@ public class AlarmDetails extends GBActivity {
}
private boolean supportsSmartWakeup() {
if (device != null) {
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
return coordinator.supportsSmartWakeup(device);
}
return false;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {

View File

@ -14,6 +14,7 @@ import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.adapter.GBAlarmListAdapter;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_ALARMS;
@ -26,6 +27,7 @@ public class ConfigureAlarms extends GBActivity {
private GBAlarmListAdapter mGBAlarmListAdapter;
private Set<String> preferencesAlarmListSet;
private boolean avoidSendAlarmsToDevice;
private GBDevice device;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -33,6 +35,8 @@ public class ConfigureAlarms extends GBActivity {
setContentView(R.layout.activity_configure_alarms);
device = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
Prefs prefs = GBApplication.getPrefs();
preferencesAlarmListSet = prefs.getStringSet(PREF_MIBAND_ALARMS, new HashSet<String>());
if (preferencesAlarmListSet.isEmpty()) {
@ -86,12 +90,16 @@ public class ConfigureAlarms extends GBActivity {
public void configureAlarm(GBAlarm alarm) {
avoidSendAlarmsToDevice = true;
Intent startIntent;
startIntent = new Intent(getApplicationContext(), AlarmDetails.class);
Intent startIntent = new Intent(getApplicationContext(), AlarmDetails.class);
startIntent.putExtra("alarm", alarm);
startIntent.putExtra(GBDevice.EXTRA_DEVICE, getDevice());
startActivityForResult(startIntent, REQ_CONFIGURE_ALARM);
}
private GBDevice getDevice() {
return device;
}
private void sendAlarmsToDevice() {
GBApplication.deviceService().onSetAlarms(mGBAlarmListAdapter.getAlarmList());
}

View File

@ -278,6 +278,7 @@ public class ControlCenter extends GBActivity {
if (selectedDevice != null) {
Intent startIntent;
startIntent = new Intent(ControlCenter.this, ConfigureAlarms.class);
startIntent.putExtra(GBDevice.EXTRA_DEVICE, selectedDevice);
startActivity(startIntent);
}
return true;

View File

@ -180,9 +180,10 @@ public class DebugActivity extends GBActivity {
@Override
public void onClick(View v) {
MusicSpec musicSpec = new MusicSpec();
musicSpec.artist = editContent.getText().toString() + "(artist)";
musicSpec.album = editContent.getText().toString() + "(album)";
musicSpec.track = editContent.getText().toString() + "(track)";
String testString = editContent.getText().toString();
musicSpec.artist = testString + "(artist)";
musicSpec.album = testString + "(album)";
musicSpec.track = testString + "(track)";
musicSpec.duration = 10;
musicSpec.trackCount = 5;
musicSpec.trackNr = 2;

View File

@ -509,7 +509,7 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
Class<? extends Activity> pairingActivity = coordinator.getPairingActivity();
if (pairingActivity != null) {
Intent intent = new Intent(this, pairingActivity);
intent.putExtra(DeviceCoordinator.EXTRA_DEVICE_MAC_ADDRESS, deviceCandidate.getMacAddress());
intent.putExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE, deviceCandidate);
startActivity(intent);
} else {
try {

View File

@ -4,6 +4,7 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.Fragment;
@ -299,6 +300,7 @@ public abstract class AbstractAppManagerFragment extends Fragment {
if (!PebbleProtocol.UUID_WEATHER.equals(selectedApp.getUUID())) {
menu.removeItem(R.id.appmanager_weather_activate);
menu.removeItem(R.id.appmanager_weather_deactivate);
menu.removeItem(R.id.appmanager_weather_install_provider);
}
if (selectedApp.getType() == GBDeviceApp.Type.APP_SYSTEM || selectedApp.getType() == GBDeviceApp.Type.WATCHFACE_SYSTEM) {
menu.removeItem(R.id.appmanager_app_delete);
@ -306,6 +308,18 @@ public abstract class AbstractAppManagerFragment extends Fragment {
if (!selectedApp.isConfigurable()) {
menu.removeItem(R.id.appmanager_app_configure);
}
if (PebbleProtocol.UUID_WEATHER.equals(selectedApp.getUUID())) {
PackageManager pm = getActivity().getPackageManager();
try {
pm.getPackageInfo("ru.gelin.android.weather.notification", PackageManager.GET_ACTIVITIES);
menu.removeItem(R.id.appmanager_weather_install_provider);
} catch (PackageManager.NameNotFoundException e) {
menu.removeItem(R.id.appmanager_weather_activate);
menu.removeItem(R.id.appmanager_weather_deactivate);
}
}
switch (selectedApp.getType()) {
case WATCHFACE:
case APP_GENERIC:
@ -385,6 +399,9 @@ public abstract class AbstractAppManagerFragment extends Fragment {
case R.id.appmanager_weather_deactivate:
GBApplication.deviceService().onAppDelete(selectedApp.getUUID());
return true;
case R.id.appmanager_weather_install_provider:
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://f-droid.org/app/ru.gelin.android.weather.notification")));
return true;
case R.id.appmanager_app_configure:
GBApplication.deviceService().onAppStart(selectedApp.getUUID(), true);

View File

@ -1,16 +1,19 @@
package nodomain.freeyourgadget.gadgetbridge.activities.appmanager;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.NavUtils;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.view.ViewPager;
import android.view.MenuItem;
import android.view.View;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -28,6 +31,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractFragmentPagerAdapter;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragmentActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
@ -35,6 +39,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
public class AppManagerActivity extends AbstractGBFragmentActivity {
private static final Logger LOG = LoggerFactory.getLogger(AbstractAppManagerFragment.class);
private int READ_REQUEST_CODE = 42;
private GBDevice mGBDevice = null;
@ -68,6 +73,18 @@ public class AppManagerActivity extends AbstractGBFragmentActivity {
throw new IllegalArgumentException("Must provide a device when invoking this activity");
}
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
assert fab != null;
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
startActivityForResult(intent, READ_REQUEST_CODE);
}
});
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(GBApplication.ACTION_QUIT);
filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
@ -93,7 +110,7 @@ public class AppManagerActivity extends AbstractGBFragmentActivity {
public class SectionsPagerAdapter extends AbstractFragmentPagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
@ -179,6 +196,16 @@ public class AppManagerActivity extends AbstractGBFragmentActivity {
return uuids;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
Intent startIntent = new Intent(AppManagerActivity.this, FwAppInstallerActivity.class);
startIntent.setAction(Intent.ACTION_VIEW);
startIntent.setDataAndType(resultData.getData(), null);
startActivity(startIntent);
}
}
@Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);

View File

@ -16,7 +16,7 @@ public class AppManagerFragmentInstalledApps extends AbstractAppManagerFragment
//systemApps.add(new GBDeviceApp(UUID.fromString("4dab81a6-d2fc-458a-992c-7a1f3b96a970"), "Sports (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
//systemApps.add(new GBDeviceApp(UUID.fromString("cf1e816a-9db0-4511-bbb8-f60c48ca8fac"), "Golf (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
systemApps.add(new GBDeviceApp(UUID.fromString("1f03293d-47af-4f28-b960-f2b02a6dd757"), "Music (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
systemApps.add(new GBDeviceApp(UUID.fromString("b2cae818-10f8-46df-ad2b-98ad2254a3c1"), "Notifications (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
systemApps.add(new GBDeviceApp(PebbleProtocol.UUID_NOTIFICATIONS, "Notifications (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
systemApps.add(new GBDeviceApp(UUID.fromString("67a32d95-ef69-46d4-a0b9-854cc62f97f9"), "Alarms (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
systemApps.add(new GBDeviceApp(UUID.fromString("18e443ce-38fd-47c8-84d5-6d0c775fbe55"), "Watchfaces (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));

View File

@ -63,10 +63,12 @@ public class PebbleContentProvider extends ContentProvider {
if (prefs.getBoolean("pebble_enable_pebblekit", false)) {
appMessage = 1;
}
String fwString = "unknown";
if (mGBDevice != null && mGBDevice.getType() == DeviceType.PEBBLE && mGBDevice.isInitialized()) {
connected = 1;
fwString = mGBDevice.getFirmwareVersion();
}
mc.addRow(new Object[]{connected, appMessage, 0, 3, 8, 2, "Gadgetbridge"});
mc.addRow(new Object[]{connected, appMessage, 0, 3, 8, 2, fwString});
return mc;
} else {

View File

@ -27,7 +27,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
* the given device.
*/
public interface DeviceCoordinator {
String EXTRA_DEVICE_MAC_ADDRESS = "nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate.EXTRA_MAC_ADDRESS";
String EXTRA_DEVICE_CANDIDATE = "nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate.EXTRA_DEVICE_CANDIDATE";
/**
* Checks whether this coordinator handles the given candidate.
@ -154,6 +154,12 @@ public interface DeviceCoordinator {
*/
boolean supportsAlarmConfiguration();
/**
* Returns true if this device/coordinator supports alarms with smart wakeup
* @return
*/
boolean supportsSmartWakeup(GBDevice device);
/**
* Returns true if the given device supports heart rate measurements.
* @return

View File

@ -22,6 +22,8 @@ import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
public interface EventHandler {
void onNotification(NotificationSpec notificationSpec);
void onDeleteNotification(int id);
void onSetTime();
void onSetAlarms(ArrayList<? extends Alarm> alarms);

View File

@ -135,6 +135,11 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
return false;
}
@Override
public boolean supportsSmartWakeup(GBDevice device) {
return false;
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return false;

View File

@ -1,12 +1,13 @@
package nodomain.freeyourgadget.gadgetbridge.devices.hplus;
/*
* @author João Paulo Barraca &lt;jpbarraca@gmail.com&gt;
*/
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* Message constants reverse-engineered by João Paulo Barraca, jpbarraca@gmail.com.
*
* @author João Paulo Barraca &lt;jpbarraca@gmail.com&gt;
*/
public final class HPlusConstants {
public static final UUID UUID_CHARACTERISTIC_CONTROL = UUID.fromString("14702856-620a-3973-7c78-9cfff0876abd");
@ -14,79 +15,95 @@ public final class HPlusConstants {
public static final UUID UUID_SERVICE_HP = UUID.fromString("14701820-620a-3973-7c78-9cfff0876abd");
public static final byte COUNTRY_CN = 1;
public static final byte COUNTRY_OTHER = 2;
public static final byte ARG_LANGUAGE_CN = 1;
public static final byte ARG_LANGUAGE_EN = 2;
public static final byte CLOCK_24H = 0;
public static final byte CLOCK_12H = 1;
public static final byte ARG_TIMEMODE_24H = 0;
public static final byte ARG_TIMEMODE_12H = 1;
public static final byte UNIT_METRIC = 0;
public static final byte UNIT_IMPERIAL = 1;
public static final byte ARG_UNIT_METRIC = 0;
public static final byte ARG_UNIT_IMPERIAL = 1;
public static final byte SEX_MALE = 0;
public static final byte SEX_FEMALE = 1;
public static final byte ARG_GENDER_MALE = 0;
public static final byte ARG_GENDER_FEMALE = 1;
public static final byte HEARTRATE_MEASURE_ON = 11;
public static final byte HEARTRATE_MEASURE_OFF = 22;
public static final byte ARG_HEARTRATE_MEASURE_ON = 11;
public static final byte ARG_HEARTRATE_MEASURE_OFF = 22;
public static final byte HEARTRATE_ALLDAY_ON = 10;
public static final byte HEARTRATE_ALLDAY_OFF = -1;
public static final byte ARG_HEARTRATE_ALLDAY_ON = 0x0A;
public static final byte ARG_HEARTRATE_ALLDAY_OFF = (byte) 0xff;
public static final byte[] COMMAND_SET_INIT1 = new byte[]{0x50,0x00,0x25,(byte) 0xb1,0x4a,0x00,0x00,0x27,0x10,0x05,0x02,0x00,(byte) 0xff,0x0a,(byte) 0xff,0x00,(byte) 0xff,(byte) 0xff,0x00,0x01};
public static final byte[] COMMAND_SET_INIT2 = new byte[]{0x51,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x07,(byte) 0xe0,0x0c,0x12,0x16,0x0a,0x10,0x00,0x00,0x00,0x00};
public static final byte INCOMING_CALL_STATE_DISABLED_THRESHOLD = 0x7B;
public static final byte INCOMING_CALL_STATE_ENABLED = (byte) 0xAA;
public static final byte[] COMMAND_SET_PREF_START = new byte[]{0x4f, 0x5a};
public static final byte[] COMMAND_SET_PREF_START1 = new byte[]{0x4d};
public static final byte[] CMD_SET_PREF_START = new byte[]{0x4f, 0x5a};
public static final byte[] CMD_SET_PREF_START1 = new byte[]{0x4d};
public static final byte CMD_SET_ALARM = 0x4c;
public static final byte CMD_SET_LANGUAGE = 0x22;
public static final byte CMD_SET_TIMEMODE = 0x47;
public static final byte CMD_SET_UNITS = 0x48;
public static final byte CMD_SET_GENDER = 0x2d;
public static final byte CMD_SET_DATE = 0x08;
public static final byte CMD_SET_TIME = 0x09;
public static final byte CMD_SET_WEEK = 0x2a;
public static final byte CMD_SET_PREF_SIT = 0x1e;
public static final byte CMD_SET_WEIGHT = 0x05;
public static final byte CMD_HEIGHT = 0x04;
public static final byte CMD_SET_AGE = 0x2c;
public static final byte CMD_SET_GOAL = 0x26;
public static final byte CMD_SET_SCREENTIME = 0x0b;
public static final byte CMD_SET_BLOOD = 0x4e; //??
public static final byte COMMAND_SET_PREF_COUNTRY = 0x22;
public static final byte COMMAND_SET_PREF_TIMEMODE = 0x47;
public static final byte COMMAND_SET_PREF_UNIT = 0x48;
public static final byte COMMAND_SET_PREF_SEX = 0x2d;
public static final byte CMD_SET_FINDME = 0x0a;
public static final byte ARG_FINDME_ON = 0x01;
public static final byte ARG_FINDME_OFF = 0x02;
public static final byte COMMAND_SET_PREF_DATE = 0x08;
public static final byte COMMAND_SET_PREF_TIME = 0x09;
public static final byte COMMAND_SET_PREF_WEEK = 0x2a;
public static final byte COMMAND_SET_PREF_SIT = 0x1e;
public static final byte COMMAND_SET_PREF_WEIGHT = 0x05;
public static final byte COMMAND_SET_PREF_HEIGHT = 0x04;
public static final byte COMMAND_SET_PREF_AGE = 0x2c;
public static final byte COMMAND_SET_PREF_GOAL = 0x26;
public static final byte COMMAND_SET_PREF_SCREENTIME = 0x0b;
public static final byte COMMAND_SET_PREF_BLOOD = 0x4e; //??
public static final byte COMMAND_SET_PREF_FINDME = 0x0a;
public static final byte COMMAND_SET_PREF_SAVE = 0x17;
public static final byte COMMAND_SET_PREF_END = 0x4f;
public static final byte COMMAND_SET_DISPLAY_ALERT = 0x23;
public static final byte COMMAND_SET_PREF_ALLDAYHR = 53;
public static final byte CMD_GET_VERSION = 0x17;
public static final byte CMD_SET_END = 0x4f;
public static final byte CMD_SET_INCOMING_CALL_NUMBER = 0x23;
public static final byte CMD_SET_ALLDAY_HRM = 0x35;
public static final byte CMD_ACTION_INCOMING_CALL = 0x41;
public static final byte CMD_SET_CONF_END = 0x4f;
public static final byte CMD_SET_PREFS = 0x50;
public static final byte CMD_SET_SIT_INTERVAL = 0x51;
public static final byte CMD_SET_HEARTRATE_STATE = 0x32;
public static final byte COMMAND_SET_INCOMING_CALL = 0x41;
public static final byte[] COMMAND_FACTORY_RESET = new byte[] {-74, 90};
//Actions to device
public static final byte CMD_GET_ACTIVE_DAY = 0x27;
public static final byte CMD_GET_DAY_DATA = 0x15;
public static final byte CMD_GET_SLEEP = 0x19;
public static final byte CMD_GET_CURR_DATA = 0x16;
public static final byte CMD_GET_DEVICE_ID = 0x24;
public static final byte COMMAND_SET_CONF_SAVE = 0x17;
public static final byte COMMAND_SET_CONF_END = 0x4f;
public static final byte CMD_ACTION_INCOMING_SOCIAL = 0x31;
//public static final byte COMMAND_ACTION_INCOMING_SMS = 0x40; //Unknown
public static final byte CMD_ACTION_DISPLAY_TEXT = 0x43;
public static final byte COMMAND_SET_PREFS = 0x50;
public static final byte COMMAND_SET_SIT_INTERVAL = 0x51;
public static final byte CMD_ACTION_DISPLAY_TEXT_NAME = 0x3F;
public static final byte CMD_ACTION_DISPLAY_TEXT_NAME_CN = 0x3E; //Text in GB2312?
public static final byte[] CMD_ACTION_HELLO = new byte[]{0x01, 0x00};
public static final byte CMD_SHUTDOWN = 0x5B;
public static final byte ARG_SHUTDOWN_EN = 0x5A;
public static final byte CMD_FACTORY_RESET = -74;
public static final byte ARG_FACTORY_RESET_EN = 0x5A;
public static final byte CMD_SET_INCOMING_MESSAGE = 0x07;
public static final byte CMD_SET_INCOMING_CALL = 0x06;
public static final byte ARG_INCOMING_CALL = (byte) -86;
public static final byte ARG_INCOMING_MESSAGE = (byte) -86;
//Incoming Messages
public static final byte DATA_STATS = 0x33;
public static final byte DATA_STEPS = 0x36;
public static final byte DATA_DAY_SUMMARY = 0x38;
public static final byte DATA_DAY_SUMMARY_ALT = 0x39;
public static final byte DATA_SLEEP = 0x1A;
public static final byte COMMAND_ACTION_INCOMING_SOCIAL = 0x31;
public static final byte COMMAND_ACTION_INCOMING_SMS = 0x40;
public static final byte COMMAND_ACTION_DISPLAY_TEXT = 0x43;
public static final byte[] COMMAND_ACTION_INCOMING_CALL = new byte[] {6, -86};
public static final byte COMMAND_ACTION_DISPLAY_TEXT_CENTER = 0x23;
public static final byte COMMAND_ACTION_DISPLAY_TEXT_NAME = 0x3F;
public static final byte COMMAND_ACTION_DISPLAY_TEXT_NAME_CN = 0x3E; //Text in GB2312?
public static final byte DATA_VERSION = 0x18;
public static final String PREF_HPLUS_SCREENTIME = "hplus_screentime";
public static final String PREF_HPLUS_ALLDAYHR = "hplus_alldayhr";
public static final String PREF_HPLUS_HR = "hplus_hr_enable";
public static final String PREF_HPLUS_UNIT = "hplus_unit";
public static final String PREF_HPLUS_TIMEMODE = "hplus_timemode";
public static final String PREF_HPLUS_WRIST = "hplus_wrist";
@ -94,6 +111,62 @@ public final class HPlusConstants {
public static final String PREF_HPLUS_ALERT_TIME = "hplus_alert_time";
public static final String PREF_HPLUS_SIT_START_TIME = "hplus_sit_start_time";
public static final String PREF_HPLUS_SIT_END_TIME = "hplus_sit_end_time";
public static final String PREF_HPLUS_COUNTRY = "hplus_country";
public static final String PREF_HPLUS_LANGUAGE = "hplus_language";
public static final Map<Character, Byte> transliterateMap = new HashMap<Character, Byte>(){
{
//These are missing
put('ó', new Byte((byte) 111));
put('Ó', new Byte((byte) 79));
put('í', new Byte((byte) 105));
put('Í', new Byte((byte) 73));
put('ú', new Byte((byte) 117));
put('Ú', new Byte((byte) 85));
//These mostly belong to the extended ASCII table
put('Ç', new Byte((byte) 128));
put('ü', new Byte((byte) 129));
put('é', new Byte((byte) 130));
put('â', new Byte((byte) 131));
put('ä', new Byte((byte) 132));
put('à', new Byte((byte) 133));
put('ã', new Byte((byte) 134));
put('ç', new Byte((byte) 135));
put('ê', new Byte((byte) 136));
put('ë', new Byte((byte) 137));
put('è', new Byte((byte) 138));
put('Ï', new Byte((byte) 139));
put('Î', new Byte((byte) 140));
put('Ì', new Byte((byte) 141));
put('Ã', new Byte((byte) 142));
put('Ä', new Byte((byte) 143));
put('É', new Byte((byte) 144));
put('æ', new Byte((byte) 145));
put('Æ', new Byte((byte) 146));
put('ô', new Byte((byte) 147));
put('ö', new Byte((byte) 148));
put('ò', new Byte((byte) 149));
put('û', new Byte((byte) 150));
put('ù', new Byte((byte) 151));
put('ÿ', new Byte((byte) 152));
put('Ö', new Byte((byte) 153));
put('Ü', new Byte((byte) 154));
put('¢', new Byte((byte) 155));
put('£', new Byte((byte) 156));
put('¥', new Byte((byte) 157));
put('ƒ', new Byte((byte) 159));
put('á', new Byte((byte) 160));
put('ñ', new Byte((byte) 164));
put('Ñ', new Byte((byte) 165));
put('ª', new Byte((byte) 166));
put('º', new Byte((byte) 167));
put('¿', new Byte((byte) 168));
put('¬', new Byte((byte) 170));
put('½', new Byte((byte) 171));
put('¼', new Byte((byte) 172));
put('¡', new Byte((byte) 173));
put('«', new Byte((byte) 174));
put('»', new Byte((byte) 175));
}
};
}

View File

@ -13,6 +13,7 @@ import android.os.Build;
import android.os.ParcelUuid;
import android.support.annotation.NonNull;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
@ -20,9 +21,9 @@ import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.UserInfo;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
@ -37,27 +38,22 @@ import java.util.Collection;
import java.util.Collections;
public class HPlusCoordinator extends AbstractDeviceCoordinator {
private static final Logger LOG = LoggerFactory.getLogger(HPlusCoordinator.class);
private static Prefs prefs = GBApplication.getPrefs();
protected static final Logger LOG = LoggerFactory.getLogger(HPlusCoordinator.class);
protected static Prefs prefs = GBApplication.getPrefs();
@NonNull
@Override
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public Collection<? extends ScanFilter> createBLEScanFilters() {
ParcelUuid mi2Service = new ParcelUuid(HPlusConstants.UUID_SERVICE_HP);
ScanFilter filter = new ScanFilter.Builder().setServiceUuid(mi2Service).build();
ParcelUuid hpService = new ParcelUuid(HPlusConstants.UUID_SERVICE_HP);
ScanFilter filter = new ScanFilter.Builder().setServiceUuid(hpService).build();
return Collections.singletonList(filter);
}
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
if (candidate.supportsService(HPlusConstants.UUID_SERVICE_HP)) {
return DeviceType.HPLUS;
}
String name = candidate.getDevice().getName();
LOG.debug("Looking for: " + name);
if (name != null && name.startsWith("HPLUS")) {
return DeviceType.HPLUS;
}
@ -97,7 +93,7 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator {
@Override
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
return new HPlusSampleProvider(device, session);
return new HPlusHealthSampleProvider(device, session);
}
@Override
@ -110,6 +106,11 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator {
return true;
}
@Override
public boolean supportsSmartWakeup(GBDevice device) {
return false;
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return true;
@ -137,7 +138,9 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator {
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
// nothing to delete, yet
Long deviceId = device.getId();
QueryBuilder<?> qb = session.getHPlusHealthActivitySampleDao().queryBuilder();
qb.where(HPlusHealthActivitySampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
}
public static int getFitnessGoal(String address) throws IllegalArgumentException {
@ -146,8 +149,8 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator {
return activityUser.getStepsGoal();
}
public static byte getCountry(String address) {
return (byte) prefs.getInt(HPlusConstants.PREF_HPLUS_COUNTRY + "_" + address, 10);
public static byte getLanguage(String address) {
return (byte) prefs.getInt(HPlusConstants.PREF_HPLUS_LANGUAGE + "_" + address, HPlusConstants.ARG_LANGUAGE_EN);
}
@ -177,10 +180,13 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator {
return (byte) (activityUser.getAge() & 0xFF);
}
public static byte getUserSex(String address) {
public static byte getUserGender(String address) {
ActivityUser activityUser = new ActivityUser();
return (byte) (activityUser.getGender() & 0xFF);
if (activityUser.getGender() == ActivityUser.GENDER_MALE)
return HPlusConstants.ARG_GENDER_MALE;
return HPlusConstants.ARG_GENDER_FEMALE;
}
public static int getGoal(String address) {
@ -194,7 +200,11 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator {
}
public static byte getAllDayHR(String address) {
return (byte) (prefs.getInt(HPlusConstants.PREF_HPLUS_ALLDAYHR + "_" + address, 10) & 0xFF);
return (byte) (prefs.getInt(HPlusConstants.PREF_HPLUS_ALLDAYHR + "_" + address, HPlusConstants.ARG_HEARTRATE_ALLDAY_ON) & 0xFF);
}
public static byte getHRState(String address) {
return (byte) (prefs.getInt(HPlusConstants.PREF_HPLUS_HR + "_" + address, HPlusConstants.ARG_HEARTRATE_MEASURE_ON) & 0xFF);
}
public static byte getSocial(String address) {

View File

@ -0,0 +1,223 @@
package nodomain.freeyourgadget.gadgetbridge.devices.hplus;
/*
* @author João Paulo Barraca &lt;jpbarraca@gmail.com&gt;
*/
import android.support.annotation.NonNull;
import android.util.Log;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.GregorianCalendar;
import java.util.List;
import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.Property;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivityOverlay;
import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivityOverlayDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.service.devices.hplus.HPlusDataRecord;
public class HPlusHealthSampleProvider extends AbstractSampleProvider<HPlusHealthActivitySample> {
private GBDevice mDevice;
private DaoSession mSession;
public HPlusHealthSampleProvider(GBDevice device, DaoSession session) {
super(device, session);
mSession = session;
mDevice = device;
}
public int getID() {
return SampleProvider.PROVIDER_HPLUS;
}
public int normalizeType(int rawType) {
switch (rawType){
case HPlusDataRecord.TYPE_DAY_SLOT:
case HPlusDataRecord.TYPE_DAY_SUMMARY:
case HPlusDataRecord.TYPE_REALTIME:
case HPlusDataRecord.TYPE_SLEEP:
case HPlusDataRecord.TYPE_UNKNOWN:
return ActivityKind.TYPE_UNKNOWN;
default:
return rawType;
}
}
public int toRawActivityKind(int activityKind) {
switch (activityKind){
case ActivityKind.TYPE_DEEP_SLEEP:
return ActivityKind.TYPE_DEEP_SLEEP;
case ActivityKind.TYPE_LIGHT_SLEEP:
return ActivityKind.TYPE_LIGHT_SLEEP;
default:
return HPlusDataRecord.TYPE_DAY_SLOT;
}
}
@NonNull
@Override
protected Property getTimestampSampleProperty() {
return HPlusHealthActivitySampleDao.Properties.Timestamp;
}
@Override
public HPlusHealthActivitySample createActivitySample() {
return new HPlusHealthActivitySample();
}
@Override
protected Property getRawKindSampleProperty() {
return null; // HPlusHealthActivitySampleDao.Properties.RawKind;
}
@Override
public float normalizeIntensity(int rawIntensity) {
return rawIntensity / (float) 100.0;
}
@NonNull
@Override
protected Property getDeviceIdentifierSampleProperty() {
return HPlusHealthActivitySampleDao.Properties.DeviceId;
}
@Override
public AbstractDao<HPlusHealthActivitySample, ?> getSampleDao() {
return getSession().getHPlusHealthActivitySampleDao();
}
public List<HPlusHealthActivitySample> getActivityamples(int timestamp_from, int timestamp_to) {
return getAllActivitySamples(timestamp_from, timestamp_to);
}
public List<HPlusHealthActivitySample> getSleepSamples(int timestamp_from, int timestamp_to) {
return getAllActivitySamples(timestamp_from, timestamp_to);
}
@NonNull
@Override
public List<HPlusHealthActivitySample> getAllActivitySamples(int timestamp_from, int timestamp_to) {
List<HPlusHealthActivitySample> samples = super.getGBActivitySamples(timestamp_from, timestamp_to, ActivityKind.TYPE_ALL);
Device dbDevice = DBHelper.findDevice(getDevice(), getSession());
if (dbDevice == null) {
return Collections.emptyList();
}
QueryBuilder<HPlusHealthActivityOverlay> qb = getSession().getHPlusHealthActivityOverlayDao().queryBuilder();
qb.where(HPlusHealthActivityOverlayDao.Properties.DeviceId.eq(dbDevice.getId()),
HPlusHealthActivityOverlayDao.Properties.TimestampFrom.ge(timestamp_from - 3600 * 24),
HPlusHealthActivityOverlayDao.Properties.TimestampTo.le(timestamp_to),
HPlusHealthActivityOverlayDao.Properties.TimestampTo.ge(timestamp_from));
List<HPlusHealthActivityOverlay> overlayRecords = qb.build().list();
//Todays sample steps will come from the Day Slots messages
//Historical steps will be provided by Day Summaries messages
//This will allow both week and current day results to be consistent
Calendar today = GregorianCalendar.getInstance();
today.set(Calendar.HOUR_OF_DAY, 0);
today.set(Calendar.MINUTE, 0);
today.set(Calendar.SECOND, 0);
today.set(Calendar.MILLISECOND, 0);
int stepsTodayMax = 0;
int stepsTodayCount = 0;
HPlusHealthActivitySample lastSample = null;
for(HPlusHealthActivitySample sample: samples){
if(sample.getTimestamp() >= today.getTimeInMillis() / 1000){
/**Strategy is:
* Calculate max steps from realtime messages
* Calculate sum of steps from day 10 minute slot summaries
*/
if(sample.getRawKind() == HPlusDataRecord.TYPE_REALTIME) {
stepsTodayMax = Math.max(stepsTodayMax, sample.getSteps());
}else if(sample.getRawKind() == HPlusDataRecord.TYPE_DAY_SLOT) {
stepsTodayCount += sample.getSteps();
}
sample.setSteps(ActivitySample.NOT_MEASURED);
lastSample = sample;
}else{
if (sample.getRawKind() != HPlusDataRecord.TYPE_DAY_SUMMARY) {
sample.setSteps(ActivitySample.NOT_MEASURED);
}
}
}
if(lastSample != null)
lastSample.setSteps(Math.max(stepsTodayCount, stepsTodayMax));
for (HPlusHealthActivityOverlay overlay : overlayRecords) {
//Create fake events to improve activity counters if there are no events around the overlay
//timestamp boundaries
//Insert one before, one at the beginning, one at the end, and one 1s after.
insertVirtualItem(samples, Math.max(overlay.getTimestampFrom() - 1, timestamp_from), overlay.getDeviceId(), overlay.getUserId());
insertVirtualItem(samples, Math.max(overlay.getTimestampFrom(), timestamp_from), overlay.getDeviceId(), overlay.getUserId());
insertVirtualItem(samples, Math.min(overlay.getTimestampTo() - 1, timestamp_to - 1), overlay.getDeviceId(), overlay.getUserId());
insertVirtualItem(samples, Math.min(overlay.getTimestampTo(), timestamp_to), overlay.getDeviceId(), overlay.getUserId());
for (HPlusHealthActivitySample sample : samples) {
if (sample.getTimestamp() >= overlay.getTimestampFrom() && sample.getTimestamp() < overlay.getTimestampTo()) {
sample.setRawKind(overlay.getRawKind());
}
}
}
detachFromSession();
Collections.sort(samples, new Comparator<HPlusHealthActivitySample>() {
public int compare(HPlusHealthActivitySample one, HPlusHealthActivitySample other) {
return one.getTimestamp() - other.getTimestamp();
}
});
return samples;
}
private List<HPlusHealthActivitySample> insertVirtualItem(List<HPlusHealthActivitySample> samples, int timestamp, long deviceId, long userId) {
HPlusHealthActivitySample sample = new HPlusHealthActivitySample(
timestamp, // ts
deviceId,
userId, // User id
null, // Raw Data
ActivityKind.TYPE_UNKNOWN,
1, // Intensity
ActivitySample.NOT_MEASURED, // Steps
ActivitySample.NOT_MEASURED, // HR
ActivitySample.NOT_MEASURED, // Distance
ActivitySample.NOT_MEASURED // Calories
);
sample.setProvider(this);
samples.add(sample);
return samples;
}
}

View File

@ -1,82 +0,0 @@
package nodomain.freeyourgadget.gadgetbridge.devices.hplus;
/*
* @author João Paulo Barraca &lt;jpbarraca@gmail.com&gt;
*/
import android.content.Context;
import android.support.annotation.NonNull;
import java.util.Collections;
import java.util.List;
import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
public class HPlusSampleProvider extends AbstractSampleProvider<HPlusHealthActivitySample> {
private GBDevice mDevice;
private DaoSession mSession;
public HPlusSampleProvider(GBDevice device, DaoSession session) {
super(device, session);
mSession = session;
mDevice = device;;
}
public int getID() {
return SampleProvider.PROVIDER_HPLUS;
}
public int normalizeType(int rawType) {
return rawType;
}
public int toRawActivityKind(int activityKind) {
return activityKind;
}
@NonNull
@Override
protected de.greenrobot.dao.Property getTimestampSampleProperty() {
return HPlusHealthActivitySampleDao.Properties.Timestamp;
}
@Override
public HPlusHealthActivitySample createActivitySample() {
return new HPlusHealthActivitySample();
}
@Override
protected de.greenrobot.dao.Property getRawKindSampleProperty() {
return HPlusHealthActivitySampleDao.Properties.RawKind;
}
@Override
public float normalizeIntensity(int rawIntensity) {
return rawIntensity; //TODO: Calculate actual value
}
@NonNull
@Override
protected de.greenrobot.dao.Property getDeviceIdentifierSampleProperty() {
return HPlusHealthActivitySampleDao.Properties.DeviceId;
}
@Override
public AbstractDao<HPlusHealthActivitySample, ?> getSampleDao() {
return getSession().getHPlusHealthActivitySampleDao();
}
}

View File

@ -0,0 +1,33 @@
package nodomain.freeyourgadget.gadgetbridge.devices.hplus;
/*
* @author João Paulo Barraca &lt;jpbarraca@gmail.com&gt;
*/
import android.support.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
/**
* Pseudo Coordinator for the Makibes F68, a sub type of the HPLUS devices
*/
public class MakibesF68Coordinator extends HPlusCoordinator {
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
String name = candidate.getDevice().getName();
if(name != null && name.startsWith("SPORT")){
return DeviceType.MAKIBESF68;
}
return DeviceType.UNKNOWN;
}
@Override
public DeviceType getDeviceType() {
return DeviceType.MAKIBESF68;
}
}

View File

@ -72,6 +72,11 @@ public class LiveviewCoordinator extends AbstractDeviceCoordinator {
return false;
}
@Override
public boolean supportsSmartWakeup(GBDevice device) {
return false;
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return false;

View File

@ -106,4 +106,9 @@ public class MiBand2Coordinator extends MiBandCoordinator {
MiBand2FWInstallHandler handler = new MiBand2FWInstallHandler(uri, context);
return handler.isValid() ? handler : null;
}
@Override
public boolean supportsSmartWakeup(GBDevice device) {
return false;
}
}

View File

@ -19,6 +19,7 @@ public final class MiBandConst {
public static final String PREF_MIBAND_DEVICE_TIME_OFFSET_HOURS = "mi_device_time_offset_hours";
public static final String PREF_MI2_DATEFORMAT = "mi2_dateformat";
public static final String PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT = "mi2_activate_display_on_lift_wrist";
public static final String PREF_MIBAND_SETUP_BT_PAIRING = "mi_setup_bt_pairing";
public static final String ORIGIN_INCOMING_CALL = "incoming_call";

View File

@ -124,6 +124,11 @@ public class MiBandCoordinator extends AbstractDeviceCoordinator {
return true;
}
@Override
public boolean supportsSmartWakeup(GBDevice device) {
return true;
}
@Override
public boolean supportsActivityTracking() {
return true;

View File

@ -23,6 +23,9 @@ import nodomain.freeyourgadget.gadgetbridge.activities.DiscoveryActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.GBActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
@ -34,7 +37,7 @@ public class MiBandPairingActivity extends GBActivity {
private static final long DELAY_AFTER_BONDING = 1000; // 1s
private TextView message;
private boolean isPairing;
private String macAddress;
private GBDeviceCandidate deviceCandidate;
private String bondingMacAddress;
private final BroadcastReceiver mPairingReceiver = new BroadcastReceiver() {
@ -43,9 +46,9 @@ public class MiBandPairingActivity extends GBActivity {
if (GBDevice.ACTION_DEVICE_CHANGED.equals(intent.getAction())) {
GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
LOG.debug("pairing activity: device changed: " + device);
if (macAddress.equals(device.getAddress())) {
if (deviceCandidate.getMacAddress().equals(device.getAddress())) {
if (device.isInitialized()) {
pairingFinished(true, macAddress);
pairingFinished(true, deviceCandidate);
} else if (device.isConnecting() || device.isInitializing()) {
LOG.info("still connecting/initializing device...");
}
@ -74,7 +77,7 @@ public class MiBandPairingActivity extends GBActivity {
attemptToConnect();
} else {
LOG.warn("Unknown bond state for device " + device.getAddress() + ": " + bondState);
pairingFinished(false, bondingMacAddress);
pairingFinished(false, deviceCandidate);
}
}
}
@ -86,7 +89,7 @@ public class MiBandPairingActivity extends GBActivity {
new Handler(mainLooper).postDelayed(new Runnable() {
@Override
public void run() {
performPair();
performApplicationLevelPair();
}
}, DELAY_AFTER_BONDING);
}
@ -98,11 +101,11 @@ public class MiBandPairingActivity extends GBActivity {
message = (TextView) findViewById(R.id.miband_pair_message);
Intent intent = getIntent();
macAddress = intent.getStringExtra(DeviceCoordinator.EXTRA_DEVICE_MAC_ADDRESS);
if (macAddress == null && savedInstanceState != null) {
macAddress = savedInstanceState.getString(STATE_MIBAND_ADDRESS, null);
deviceCandidate = intent.getParcelableExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE);
if (deviceCandidate == null && savedInstanceState != null) {
deviceCandidate = savedInstanceState.getParcelable(STATE_MIBAND_ADDRESS);
}
if (macAddress == null) {
if (deviceCandidate == null) {
Toast.makeText(this, getString(R.string.message_cannot_pair_no_mac), Toast.LENGTH_SHORT).show();
startActivity(new Intent(this, DiscoveryActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
finish();
@ -122,13 +125,13 @@ public class MiBandPairingActivity extends GBActivity {
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(STATE_MIBAND_ADDRESS, macAddress);
outState.putParcelable(STATE_MIBAND_ADDRESS, deviceCandidate);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
macAddress = savedInstanceState.getString(STATE_MIBAND_ADDRESS, macAddress);
deviceCandidate = savedInstanceState.getParcelable(STATE_MIBAND_ADDRESS);
}
@Override
@ -145,13 +148,9 @@ public class MiBandPairingActivity extends GBActivity {
@Override
protected void onDestroy() {
try {
// just to be sure, remove the receivers -- might actually be already unregistered
LocalBroadcastManager.getInstance(this).unregisterReceiver(mPairingReceiver);
unregisterReceiver(mBondingReceiver);
} catch (IllegalArgumentException ex) {
// already unregistered, ignore
}
// just to be sure, remove the receivers -- might actually be already unregistered
AndroidUtils.safeUnregisterBroadcastReceiver(LocalBroadcastManager.getInstance(this), mPairingReceiver);
AndroidUtils.safeUnregisterBroadcastReceiver(this, mBondingReceiver);
if (isPairing) {
stopPairing();
}
@ -160,22 +159,30 @@ public class MiBandPairingActivity extends GBActivity {
private void startPairing() {
isPairing = true;
message.setText(getString(R.string.pairing, macAddress));
message.setText(getString(R.string.pairing, deviceCandidate));
IntentFilter filter = new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mPairingReceiver, filter);
if (!shouldSetupBTLevelPairing()) {
// there are connection problems on certain Galaxy S devices at least;
// try to connect without BT pairing (bonding)
attemptToConnect();
return;
}
filter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
registerReceiver(mBondingReceiver, filter);
BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(macAddress);
if (device != null) {
performBluetoothPair(device);
} else {
GB.toast(this, "No such Bluetooth Device: " + macAddress, Toast.LENGTH_LONG, GB.ERROR);
}
performBluetoothPair(deviceCandidate);
}
private void pairingFinished(boolean pairedSuccessfully, String macAddress) {
private boolean shouldSetupBTLevelPairing() {
Prefs prefs = GBApplication.getPrefs();
return prefs.getPreferences().getBoolean(MiBandConst.PREF_MIBAND_SETUP_BT_PAIRING, true);
}
private void pairingFinished(boolean pairedSuccessfully, GBDeviceCandidate candidate) {
LOG.debug("pairingFinished: " + pairedSuccessfully);
if (!isPairing) {
// already gone?
@ -183,13 +190,14 @@ public class MiBandPairingActivity extends GBActivity {
}
isPairing = false;
LocalBroadcastManager.getInstance(this).unregisterReceiver(mPairingReceiver);
unregisterReceiver(mBondingReceiver);
AndroidUtils.safeUnregisterBroadcastReceiver(LocalBroadcastManager.getInstance(this), mPairingReceiver);
AndroidUtils.safeUnregisterBroadcastReceiver(this, mBondingReceiver);
if (pairedSuccessfully) {
// remember the device since we do not necessarily pair... temporary -- we probably need
// to query the db for available devices in ControlCenter. But only remember un-bonded
// devices, as bonded devices are displayed anyway.
String macAddress = deviceCandidate.getMacAddress();
BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(macAddress);
if (device != null && device.getBondState() == BluetoothDevice.BOND_NONE) {
Prefs prefs = GBApplication.getPrefs();
@ -206,11 +214,13 @@ public class MiBandPairingActivity extends GBActivity {
isPairing = false;
}
protected void performBluetoothPair(BluetoothDevice device) {
protected void performBluetoothPair(GBDeviceCandidate deviceCandidate) {
BluetoothDevice device = deviceCandidate.getDevice();
int bondState = device.getBondState();
if (bondState == BluetoothDevice.BOND_BONDED) {
GB.toast(getString(R.string.pairing_already_bonded, device.getName(), device.getAddress()), Toast.LENGTH_SHORT, GB.INFO);
performPair();
performApplicationLevelPair();
return;
}
@ -226,8 +236,13 @@ public class MiBandPairingActivity extends GBActivity {
}
}
private void performPair() {
private void performApplicationLevelPair() {
GBApplication.deviceService().disconnect(); // just to make sure...
GBApplication.deviceService().connect(macAddress, true);
GBDevice device = DeviceHelper.getInstance().toSupportedDevice(deviceCandidate);
if (device != null) {
GBApplication.deviceService().connect(device, true);
} else {
GB.toast(this, "Unable to connect, can't recognize the device type: " + deviceCandidate, Toast.LENGTH_LONG, GB.ERROR);
}
}
}

View File

@ -25,6 +25,7 @@ import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PR
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_FITNESS_GOAL;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_USE_HR_FOR_SLEEP_DETECTION;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_SETUP_BT_PAIRING;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_USER_ALIAS;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_COUNT;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.getNotificationPrefKey;

View File

@ -145,8 +145,11 @@ public class PBWReader {
} else if (fileName.equals("appinfo.json")) {
long bytes = ze.getSize();
if (bytes > 65536) // that should be too much
if (bytes > 500000) {
LOG.warn(fileName + " exeeds maximum of 500000 bytes");
// that should be too much
break;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ((count = zis.read(buffer)) != -1) {

View File

@ -108,6 +108,11 @@ public class PebbleCoordinator extends AbstractDeviceCoordinator {
return false;
}
@Override
public boolean supportsSmartWakeup(GBDevice device) {
return false;
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return PebbleUtils.hasHRM(device.getModel());

View File

@ -7,6 +7,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v4.content.LocalBroadcastManager;
import android.widget.TextView;
import android.widget.Toast;
@ -28,6 +29,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@ -90,7 +92,10 @@ public class PebblePairingActivity extends GBActivity {
message = (TextView) findViewById(R.id.pebble_pair_message);
Intent intent = getIntent();
macAddress = intent.getStringExtra(DeviceCoordinator.EXTRA_DEVICE_MAC_ADDRESS);
GBDeviceCandidate candidate = intent.getParcelableExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE);
if (candidate != null) {
macAddress = candidate.getMacAddress();
}
if (macAddress == null) {
Toast.makeText(this, getString(R.string.message_cannot_pair_no_mac), Toast.LENGTH_SHORT).show();
returnToPairingActivity();

View File

@ -73,6 +73,11 @@ public class VibratissimoCoordinator extends AbstractDeviceCoordinator {
return false;
}
@Override
public boolean supportsSmartWakeup(GBDevice device) {
return false;
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return false;

View File

@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.model.AppNotificationType;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
@ -252,6 +253,14 @@ public class NotificationListener extends NotificationListenerService {
notificationSpec.type = AppNotificationType.getInstance().get(source);
if (source.equals("com.fsck.k9")) {
// we dont want group summaries at all for k9
if ((notification.flags & Notification.FLAG_GROUP_SUMMARY) == Notification.FLAG_GROUP_SUMMARY) {
return;
}
preferBigText = true;
}
if (notificationSpec.type == null) {
notificationSpec.type = NotificationType.UNKNOWN;
}
@ -261,6 +270,13 @@ public class NotificationListener extends NotificationListenerService {
dissectNotificationTo(notification, notificationSpec, preferBigText);
notificationSpec.id = (int) sbn.getPostTime(); //FIMXE: a truly unique id would be better
// ignore Gadgetbridge's very own notifications, except for those from the debug screen
if (getApplicationContext().getPackageName().equals(source)) {
if (!getApplicationContext().getString(R.string.test_notification).equals(notificationSpec.title)) {
return;
}
}
NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender(notification);
List<NotificationCompat.Action> actions = wearableExtender.getActions();
@ -388,7 +404,26 @@ public class NotificationListener extends NotificationListenerService {
@Override
public void onNotificationRemoved(StatusBarNotification sbn) {
//FIXME: deduplicate code
String source = sbn.getPackageName();
Notification notification = sbn.getNotification();
if ((notification.flags & Notification.FLAG_ONGOING_EVENT) == Notification.FLAG_ONGOING_EVENT) {
return;
}
if (source.equals("android") ||
source.equals("com.android.systemui") ||
source.equals("com.android.dialer") ||
source.equals("com.cyanogenmod.eleven")) {
return;
}
Prefs prefs = GBApplication.getPrefs();
if (prefs.getBoolean("autoremove_notifications", false)) {
LOG.info("notification removed, will ask device to delete it");
GBApplication.deviceService().onDeleteNotification((int) sbn.getPostTime()); //FIMXE: a truly unique id would be better
}
}
private void dumpExtras(Bundle bundle) {

View File

@ -1,9 +1,14 @@
package nodomain.freeyourgadget.gadgetbridge.externalevents;
import android.app.Activity;
import android.os.Bundle;
public class WeatherNotificationConfig extends Activity {
//TODO: we just need the user to enable us in the weather notification settings. There must be a better way
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.GBActivity;
public class WeatherNotificationConfig extends GBActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_weather_notification);
}
}

View File

@ -57,7 +57,7 @@ public class GBDeviceCandidate implements Parcelable {
dest.writeParcelable(device, 0);
dest.writeInt(rssi);
dest.writeString(deviceType.name());
dest.writeArray(serviceUuds);
dest.writeParcelableArray(serviceUuds, 0);
}
public static final Creator<GBDeviceCandidate> CREATOR = new Creator<GBDeviceCandidate>() {

View File

@ -19,10 +19,25 @@ import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService;
import nodomain.freeyourgadget.gadgetbridge.util.LanguageUtils;
public class GBDeviceService implements DeviceService {
protected final Context mContext;
private final Class<? extends Service> mServiceClass;
private final String[] transliterationExtras = new String[]{
EXTRA_NOTIFICATION_PHONENUMBER,
EXTRA_NOTIFICATION_SENDER,
EXTRA_NOTIFICATION_SUBJECT,
EXTRA_NOTIFICATION_TITLE,
EXTRA_NOTIFICATION_BODY,
EXTRA_NOTIFICATION_SOURCENAME,
EXTRA_CALL_PHONENUMBER,
EXTRA_MUSIC_ARTIST,
EXTRA_MUSIC_ALBUM,
EXTRA_MUSIC_TRACK,
EXTRA_CALENDAREVENT_TITLE,
EXTRA_CALENDAREVENT_DESCRIPTION
};
public GBDeviceService(Context context) {
mContext = context;
@ -34,6 +49,14 @@ public class GBDeviceService implements DeviceService {
}
protected void invokeService(Intent intent) {
if(LanguageUtils.transliterate()){
for (String extra: transliterationExtras) {
if (intent.hasExtra(extra)){
intent.putExtra(extra, LanguageUtils.transliterate(intent.getStringExtra(extra)));
}
}
}
mContext.startService(intent);
}
@ -53,21 +76,14 @@ public class GBDeviceService implements DeviceService {
}
@Override
public void connect(GBDevice device) {
Intent intent = createIntent().setAction(ACTION_CONNECT)
.putExtra(GBDevice.EXTRA_DEVICE, device);
invokeService(intent);
public void connect(@Nullable GBDevice device) {
connect(device, false);
}
@Override
public void connect(@Nullable String deviceAddress) {
connect(deviceAddress, false);
}
@Override
public void connect(@Nullable String deviceAddress, boolean performPair) {
@Override
public void connect(@Nullable GBDevice device, boolean performPair) {
Intent intent = createIntent().setAction(ACTION_CONNECT)
.putExtra(EXTRA_DEVICE_ADDRESS, deviceAddress)
.putExtra(GBDevice.EXTRA_DEVICE, device)
.putExtra(EXTRA_PERFORM_PAIR, performPair);
invokeService(intent);
}
@ -105,6 +121,14 @@ public class GBDeviceService implements DeviceService {
invokeService(intent);
}
@Override
public void onDeleteNotification(int id) {
Intent intent = createIntent().setAction(ACTION_DELETE_NOTIFICATION)
.putExtra(EXTRA_NOTIFICATION_ID, id);
invokeService(intent);
}
@Override
public void onSetTime() {
Intent intent = createIntent().setAction(ACTION_SETTIME);

View File

@ -16,6 +16,7 @@ public interface DeviceService extends EventHandler {
String ACTION_START = PREFIX + ".action.start";
String ACTION_CONNECT = PREFIX + ".action.connect";
String ACTION_NOTIFICATION = PREFIX + ".action.notification";
String ACTION_DELETE_NOTIFICATION = PREFIX + ".action.delete_notification";
String ACTION_CALLSTATE = PREFIX + ".action.callstate";
String ACTION_SETCANNEDMESSAGES = PREFIX + ".action.setcannedmessages";
String ACTION_SETTIME = PREFIX + ".action.settime";
@ -46,7 +47,6 @@ public interface DeviceService extends EventHandler {
String ACTION_SEND_CONFIGURATION = PREFIX + ".action.send_configuration";
String ACTION_SEND_WEATHER = PREFIX + ".action.send_weather";
String ACTION_TEST_NEW_FUNCTION = PREFIX + ".action.test_new_function";
String EXTRA_DEVICE_ADDRESS = "device_address";
String EXTRA_NOTIFICATION_BODY = "notification_body";
String EXTRA_NOTIFICATION_FLAGS = "notification_flags";
String EXTRA_NOTIFICATION_ID = "notification_id";
@ -111,17 +111,14 @@ public interface DeviceService extends EventHandler {
String EXTRA_CALENDAREVENT_DURATION = "calendarevent_duration";
String EXTRA_CALENDAREVENT_TITLE = "calendarevent_title";
String EXTRA_CALENDAREVENT_DESCRIPTION = "calendarevent_description";
String EXTRA_MIBAND2_AUTH_BYTE = "miband2_auth_byte";
void start();
void connect();
void connect(GBDevice device);
void connect(@Nullable GBDevice device);
void connect(@Nullable String deviceAddress);
void connect(@Nullable String deviceAddress, boolean performPair);
void connect(@Nullable GBDevice device, boolean performPair);
void disconnect();

View File

@ -14,6 +14,7 @@ public enum DeviceType {
VIBRATISSIMO(20),
LIVEVIEW(30),
HPLUS(40),
MAKIBESF68(41),
TEST(1000);
private final int key;

View File

@ -60,6 +60,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_CA
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_CONNECT;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_DELETEAPP;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_DELETE_CALENDAREVENT;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_DELETE_NOTIFICATION;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_DISCONNECT;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_ENABLE_HEARTRATE_SLEEP_SUPPORT;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_ENABLE_REALTIME_HEARTRATE_MEASUREMENT;
@ -100,7 +101,6 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CAL
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CANNEDMESSAGES;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CANNEDMESSAGES_TYPE;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CONFIG;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_DEVICE_ADDRESS;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_FIND_START;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_ALBUM;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_ARTIST;
@ -176,6 +176,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
String action = intent.getAction();
if (action.equals(GBDevice.ACTION_DEVICE_CHANGED)) {
GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
// FIXME: mGBDevice was null here once
if (mGBDevice.equals(device)) {
mGBDevice = device;
boolean enableReceivers = mDeviceSupport != null && (mDeviceSupport.useAutoConnect() || mGBDevice.isInitialized());
@ -275,12 +276,11 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
GBDevice gbDevice = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
String btDeviceAddress = null;
if (gbDevice == null) {
btDeviceAddress = intent.getStringExtra(EXTRA_DEVICE_ADDRESS);
if (btDeviceAddress == null && prefs != null) { // may be null in test cases
if (prefs != null) { // may be null in test cases
btDeviceAddress = prefs.getString("last_device_address", null);
}
if (btDeviceAddress != null) {
gbDevice = DeviceHelper.getInstance().findAvailableDevice(btDeviceAddress, this);
if (btDeviceAddress != null) {
gbDevice = DeviceHelper.getInstance().findAvailableDevice(btDeviceAddress, this);
}
}
} else {
btDeviceAddress = gbDevice.getAddress();
@ -326,16 +326,18 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
notificationSpec.subject = intent.getStringExtra(EXTRA_NOTIFICATION_SUBJECT);
notificationSpec.title = intent.getStringExtra(EXTRA_NOTIFICATION_TITLE);
notificationSpec.body = intent.getStringExtra(EXTRA_NOTIFICATION_BODY);
notificationSpec.sourceName = intent.getStringExtra(EXTRA_NOTIFICATION_SOURCENAME);
notificationSpec.type = (NotificationType) intent.getSerializableExtra(EXTRA_NOTIFICATION_TYPE);
notificationSpec.id = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1);
notificationSpec.flags = intent.getIntExtra(EXTRA_NOTIFICATION_FLAGS, 0);
notificationSpec.sourceName = intent.getStringExtra(EXTRA_NOTIFICATION_SOURCENAME);
if (notificationSpec.type == NotificationType.GENERIC_SMS && notificationSpec.phoneNumber != null) {
notificationSpec.sender = getContactDisplayNameByNumber(notificationSpec.phoneNumber);
notificationSpec.id = mRandom.nextInt(); // FIXME: add this in external SMS Receiver?
GBApplication.getIDSenderLookup().add(notificationSpec.id, notificationSpec.phoneNumber);
}
if (((notificationSpec.flags & NotificationSpec.FLAG_WEARABLE_REPLY) > 0)
|| (notificationSpec.type == NotificationType.GENERIC_SMS && notificationSpec.phoneNumber != null)) {
// NOTE: maybe not where it belongs
@ -352,9 +354,14 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
notificationSpec.cannedReplies = replies.toArray(new String[replies.size()]);
}
}
mDeviceSupport.onNotification(notificationSpec);
break;
}
case ACTION_DELETE_NOTIFICATION: {
mDeviceSupport.onDeleteNotification(intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1));
break;
}
case ACTION_ADD_CALENDAREVENT: {
CalendarEventSpec calendarEventSpec = new CalendarEventSpec();
calendarEventSpec.id = intent.getLongExtra(EXTRA_CALENDAREVENT_ID, -1);

View File

@ -10,6 +10,7 @@ import java.util.EnumSet;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.liveview.LiveviewSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.MiBand2Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport;
@ -98,7 +99,10 @@ public class DeviceSupportFactory {
deviceSupport = new ServiceDeviceSupport(new LiveviewSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
case HPLUS:
deviceSupport = new ServiceDeviceSupport(new HPlusSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
deviceSupport = new ServiceDeviceSupport(new HPlusSupport(DeviceType.HPLUS), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
case MAKIBESF68:
deviceSupport = new ServiceDeviceSupport(new HPlusSupport(DeviceType.MAKIBESF68), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
}
if (deviceSupport != null) {

View File

@ -135,6 +135,11 @@ public class ServiceDeviceSupport implements DeviceSupport {
delegate.onNotification(notificationSpec);
}
@Override
public void onDeleteNotification(int id) {
delegate.onDeleteNotification(id);
}
@Override
public void onSetTime() {
if (checkBusy("set time") || checkThrottle("set time")) {

View File

@ -1,7 +1,10 @@
package nodomain.freeyourgadget.gadgetbridge.service.btle;
import android.bluetooth.BluetoothGattCharacteristic;
import android.util.Log;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@ -178,64 +181,12 @@ public class GattCharacteristic {
public static final UUID UUID_CHARACTERISTIC_WIND_CHILL = UUID.fromString((String.format(AbstractBTLEDeviceSupport.BASE_UUID, "2A79")));
//do we need this?
private static Map<UUID, String> GATTCHARACTERISTIC_DEBUG;
private static final Map<UUID, String> GATTCHARACTERISTIC_DEBUG;
static {
GATTCHARACTERISTIC_DEBUG = new HashMap<>();
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_ALERT_CATEGORY_ID, "Alert AlertCategory ID");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_ALERT_CATEGORY_ID_BIT_MASK, "Alert AlertCategory ID Bit Mask");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_ALERT_LEVEL, "Alert Level");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_ALERT_NOTIFICATION_CONTROL_POINT, "Alert Notification Control Point");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_ALERT_STATUS, "Alert Status");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_GAP_APPEARANCE, "Appearance");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_BLOOD_PRESSURE_FEATURE, "Blood Pressure Feature");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_BLOOD_PRESSURE_MEASUREMENT, "Blood Pressure Measurement");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_BODY_SENSOR_LOCATION, "Body Sensor Location");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_CURRENT_TIME, "Current Time");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_DATE_TIME, "Date Time");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_DAY_DATE_TIME, "Day Date Time");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_DAY_OF_WEEK, "Day of Week");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_GAP_DEVICE_NAME, "Device Name");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_DST_OFFSET, "DST Offset");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_EXACT_TIME_256, "Exact Time 256");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_FIRMWARE_REVISION_STRING, "Firmware Revision String");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_HARDWARE_REVISION_STRING, "Hardware Revision String");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT, "Heart Rate Control Point");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT, "Heart Rate Measurement");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_IEEE_11073_20601_REGULATORY_CERTIFICATION_DATA_LIST, "IEEE 11073-20601 Regulatory");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_INTERMEDIATE_BLOOD_PRESSURE, "Intermediate Cuff Pressure");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_INTERMEDIATE_TEMPERATURE, "Intermediate Temperature");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_LOCAL_TIME_INFORMATION, "Local Time Information");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_MANUFACTURER_NAME_STRING, "Manufacturer Name String");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_MEASUREMENT_INTERVAL, "Measurement Interval");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_MODEL_NUMBER_STRING, "Model Number String");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_NEW_ALERT, "New Alert");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_GAP_PERIPHERAL_PREFERRED_CONNECTION_PARAMETERS, "Peripheral Preferred Connection Parameters");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_GAP_PERIPHERAL_PRIVACY_FLAG, "Peripheral Privacy Flag");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_GAP_RECONNECTION_ADDRESS, "Reconnection Address");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_REFERENCE_TIME_INFORMATION, "Reference Time Information");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_RINGER_CONTROL_POINT, "Ringer Control Point");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_RINGER_SETTING, "Ringer Setting");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_SERIAL_NUMBER_STRING, "Serial Number String");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_GATT_SERVICE_CHANGED, "Service Changed");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_SOFTWARE_REVISION_STRING, "Software Revision String");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_SUPPORTED_NEW_ALERT_CATEGORY, "Supported New Alert AlertCategory");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_SUPPORTED_UNREAD_ALERT_CATEGORY, "Supported Unread Alert AlertCategory");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_SYSTEM_ID, "System ID");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_TEMPERATURE_MEASUREMENT, "Temperature Measurement");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_TEMPERATURE_TYPE, "Temperature DeviceType");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_TIME_ACCURACY, "Time Accuracy");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_TIME_SOURCE, "Time Source");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_TIME_UPDATE_CONTROL_POINT, "Time Update Control Point");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_TIME_UPDATE_STATE, "Time Update State");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_TIME_WITH_DST, "Time with DST");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_TIME_ZONE, "Time Zone");
GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_TX_POWER_LEVEL, "Tx Power Level");
}
public static String lookup(UUID uuid, String fallback) {
public static synchronized String lookup(UUID uuid, String fallback) {
if (GATTCHARACTERISTIC_DEBUG == null) {
GATTCHARACTERISTIC_DEBUG = initDebugMap();
}
String name = GATTCHARACTERISTIC_DEBUG.get(uuid);
if (name == null) {
name = fallback;
@ -243,6 +194,42 @@ public class GattCharacteristic {
return name;
}
private static Map<UUID, String> initDebugMap() {
Map<UUID,String> map = new HashMap<>();
try {
for (Field field : GattCharacteristic.class.getDeclaredFields()) {
if ((field.getModifiers() & Modifier.STATIC) != 0 && field.getType() == UUID.class) {
UUID uuid = (UUID) field.get(null);
if (uuid != null) {
map.put(uuid, toPrettyName(field.getName()));
}
}
}
} catch (Exception ex) {
Log.w(GattCharacteristic.class.getName(), "Error reading UUID fields by reflection: " + ex.getMessage(), ex);
}
return map;
}
private static String toPrettyName(String fieldName) {
String[] words = fieldName.split("_");
if (words.length <= 1) {
return fieldName.toLowerCase();
}
StringBuilder builder = new StringBuilder(fieldName.length());
for (String word : words) {
if (word.length() == 0 || "UUID".equals(word) || "CHARACTERISTIC".equals(word)) {
continue;
}
if (builder.length() > 0) {
builder.append(" ");
}
builder.append(word.toLowerCase());
}
return builder.toString();
}
public static String toString(BluetoothGattCharacteristic characteristic) {
return characteristic.getUuid() + " (" + lookup(characteristic.getUuid(), "unknown") + ")";
}

View File

@ -0,0 +1,66 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
/*
* @author João Paulo Barraca &lt;jpbarraca@gmail.com&gt;
*/
public class HPlusDataRecord {
public final static int TYPE_UNKNOWN = 0;
public final static int TYPE_SLEEP = 100;
public final static int TYPE_DAY_SUMMARY = 101;
public final static int TYPE_DAY_SLOT = 102;
public final static int TYPE_REALTIME = 103;
public int type = TYPE_UNKNOWN;
public int activityKind = ActivityKind.TYPE_UNKNOWN;
/**
* Time of this record in seconds
*/
public int timestamp;
/**
* Raw data as sent from the device
*/
public byte[] rawData;
protected HPlusDataRecord(){
}
protected HPlusDataRecord(byte[] data, int type){
this.rawData = data;
this.type = type;
}
public byte[] getRawData() {
return rawData;
}
public class RecordInterval {
/**
* Start time of this interval in seconds
*/
public int timestampFrom;
/**
* End time of this interval in seconds
*/
public int timestampTo;
/**
* Type of activity {@link ActivityKind}
*/
public int activityKind;
RecordInterval(int timestampFrom, int timestampTo, int activityKind) {
this.timestampFrom = timestampFrom;
this.timestampTo = timestampTo;
this.activityKind = activityKind;
}
}
}

View File

@ -0,0 +1,89 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus;
/*
* @author João Paulo Barraca &lt;jpbarraca@gmail.com&gt;
*/
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
public class HPlusDataRecordDaySlot extends HPlusDataRecord {
/**
* The device reports data aggregated in slots.
* There are 144 slots in a given day, summarizing 10 minutes of data
* Integer with the slot number from 0 to 143
*/
public int slot;
/**
* Number of steps
*/
public int steps;
/**
* Number of seconds without activity (TBC)
*/
public int secondsInactive;
/**
* Average Heart Rate in Beats Per Minute
*/
public int heartRate;
public HPlusDataRecordDaySlot(byte[] data) {
super(data, TYPE_DAY_SLOT);
int a = (data[4] & 0xFF) * 256 + (data[5] & 0xFF);
if (a >= 144) {
throw new IllegalArgumentException("Invalid Slot Number");
}
slot = a;
heartRate = data[1] & 0xFF;
if(heartRate == 255 || heartRate == 0)
heartRate = ActivitySample.NOT_MEASURED;
steps = (data[2] & 0xFF) * 256 + (data[3] & 0xFF);
//?? data[6]; atemp?? always 0
secondsInactive = data[7] & 0xFF; // ?
Calendar slotTime = GregorianCalendar.getInstance();
slotTime.set(Calendar.MINUTE, (slot % 6) * 10);
slotTime.set(Calendar.HOUR_OF_DAY, slot / 6);
slotTime.set(Calendar.SECOND, 0);
timestamp = (int) (slotTime.getTimeInMillis() / 1000L);
}
public String toString(){
Calendar slotTime = GregorianCalendar.getInstance();
slotTime.setTimeInMillis(timestamp * 1000L);
return String.format(Locale.US, "Slot: %d, Time: %s, Steps: %d, InactiveSeconds: %d, HeartRate: %d", slot, slotTime.getTime(), steps, secondsInactive, heartRate);
}
public void accumulate(HPlusDataRecordDaySlot other){
if(other == null)
return;
if(steps == ActivitySample.NOT_MEASURED)
steps = other.steps;
else if(other.steps != ActivitySample.NOT_MEASURED)
steps += other.steps;
if(heartRate == ActivitySample.NOT_MEASURED)
heartRate = other.heartRate;
else if(other.heartRate != ActivitySample.NOT_MEASURED) {
heartRate = (heartRate + other.heartRate) / 2;
}
secondsInactive += other.secondsInactive;
}
}

View File

@ -0,0 +1,101 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus;
/*
* @author João Paulo Barraca &lt;jpbarraca@gmail.com&gt;
*/
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Locale;
class HPlusDataRecordDaySummary extends HPlusDataRecord{
/**
* Year of the record reported by the device
* Sometimes the device will report a low number (e.g, 116) which will be "corrected"
* by adding 1900
*/
public int year;
/**
* Month of the record reported by the device from 1 to 12
*/
public int month;
/**
* Day of the record reported by the device
*/
public int day;
/**
* Number of steps accumulated in the day reported
*/
public int steps;
/**
* Distance in meters accumulated in the day reported
*/
public int distance;
/**
* Amount of time active in the day (Units are To Be Determined)
*/
public int activeTime;
/**
* Max Heart Rate recorded in Beats Per Minute
*/
public int maxHeartRate;
/**
* Min Heart Rate recorded in Beats Per Minute
*/
public int minHeartRate;
/**
* Amount of estimated calories consumed during the day in KCalories
*/
public int calories;
HPlusDataRecordDaySummary(byte[] data) {
super(data, TYPE_DAY_SUMMARY);
year = (data[10] & 0xFF) * 256 + (data[9] & 0xFF);
month = data[11] & 0xFF;
day = data[12] & 0xFF;
//Recover from bug in firmware where year is corrupted
//data[10] will be set to 0, effectively offsetting values by minus 1900 years
if(year < 1900)
year += 1900;
if (year < 2000 || month > 12 || day > 31) {
throw new IllegalArgumentException("Invalid record date "+year+"-"+month+"-"+day);
}
steps = (data[2] & 0xFF) * 256 + (data[1] & 0xFF);
distance = ((data[4] & 0xFF) * 256 + (data[3] & 0xFF)) * 10;
activeTime = (data[14] & 0xFF) * 256 + (data[13] & 0xFF);
calories = (data[6] & 0xFF) * 256 + (data[5] & 0xFF);
calories += (data[8] & 0xFF) * 256 + (data[7] & 0xFF);
maxHeartRate = data[15] & 0xFF;
minHeartRate = data[16] & 0xFF;
Calendar date = GregorianCalendar.getInstance();
date.set(Calendar.YEAR, year);
date.set(Calendar.MONTH, month - 1);
date.set(Calendar.DAY_OF_MONTH, day);
date.set(Calendar.HOUR_OF_DAY, 23);
date.set(Calendar.MINUTE, 59);
date.set(Calendar.SECOND, 59);
date.set(Calendar.MILLISECOND, 999);
timestamp = (int) (date.getTimeInMillis() / 1000);
}
public String toString(){
return String.format(Locale.US, "%s-%s-%s steps:%d distance:%d minHR:%d maxHR:%d calories:%d activeTime:%d", year, month, day, steps, distance,minHeartRate, maxHeartRate, calories, activeTime);
}
}

View File

@ -0,0 +1,114 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus;
/*
* @author João Paulo Barraca &lt;jpbarraca@gmail.com&gt;
*/
import java.util.GregorianCalendar;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
class HPlusDataRecordRealtime extends HPlusDataRecord {
/**
* Distance accumulated during the day in meters
*/
public int distance;
/**
* Calories consumed during the day in KCalories
*/
public int calories;
/**
* Instantaneous Heart Rate measured in Beats Per Minute
*/
public int heartRate;
/**
* Battery level from 0 to 100
*/
public byte battery;
/**
* Number of steps today
*/
public int steps;
/**
* Time active (To be determined how it works)
*/
public int activeTime;
/**
* Computing intensity
* To be calculated appropriately
*/
public int intensity;
public HPlusDataRecordRealtime(byte[] data) {
super(data, TYPE_REALTIME);
if (data.length < 15) {
throw new IllegalArgumentException("Invalid data packet");
}
timestamp = (int) (GregorianCalendar.getInstance().getTimeInMillis() / 1000);
distance = 10 * ((data[4] & 0xFF) * 256 + (data[3] & 0xFF)); // meters
steps = (data[2] & 0xFF) * 256 + (data[1] & 0xFF);
int x = (data[6] & 0xFF) * 256 + (data[5] & 0xFF);
int y = (data[8] & 0xFF) * 256 + (data[7] & 0xFF);
battery = data[9];
calories = x + y; // KCal
heartRate = data[11] & 0xFF; // BPM
activeTime = (data[14] & 0xFF * 256) + (data[13] & 0xFF);
if(heartRate == 255) {
intensity = 0;
activityKind = ActivityKind.TYPE_NOT_MEASURED;
heartRate = ActivitySample.NOT_MEASURED;
}
else {
intensity = (int) (100 * Math.max(0, Math.min((heartRate - 60) / 120.0, 1))); // TODO: Calculate a proper value
activityKind = ActivityKind.TYPE_UNKNOWN;
}
}
public void computeActivity(HPlusDataRecordRealtime prev){
if(prev == null)
return;
int deltaDistance = distance - prev.distance;
if(deltaDistance <= 0)
return;
int deltaTime = timestamp - prev.timestamp;
if(deltaTime <= 0)
return;
double speed = deltaDistance / deltaTime;
if(speed >= 1.6) // ~6 KM/h
activityKind = ActivityKind.TYPE_ACTIVITY;
}
public boolean same(HPlusDataRecordRealtime other){
if(other == null)
return false;
return steps == other.steps && distance == other.distance && calories == other.calories && heartRate == other.heartRate && battery == other.battery;
}
public String toString(){
return String.format(Locale.US, "Distance: %d Steps: %d Calories: %d HeartRate: %d Battery: %d ActiveTime: %d Intensity: %d", distance, steps, calories, heartRate, battery, activeTime, intensity);
}
}

View File

@ -0,0 +1,129 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus;
/*
* @author João Paulo Barraca &lt;jpbarraca@gmail.com&gt;
*/
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
public class HPlusDataRecordSleep extends HPlusDataRecord {
/**
* Time which the device determined to be the bed time in seconds
*/
public int bedTimeStart;
/**
* Time which the device determined to be the end of this sleep period in seconds
*/
public int bedTimeEnd;
/**
* Number of minutes in Deep Sleep
*/
public int deepSleepMinutes;
/**
* Number of minutes in Light Sleep
* This is considered as Light Sleep
*/
public int lightSleepMinutes;
/**
* Number of minutes to start sleeping (??)
* This is considered as Light Sleep
*/
public int enterSleepMinutes;
/**
* Number of minutes with Sleep Spindles (??)
* This is considered as Light Sleep
*/
public int spindleMinutes;
/**
* Number of minutes in REM sleep
* This is considered as Light Sleep
*/
public int remSleepMinutes;
/**
* Number of wake up minutes during the sleep period
* This is not considered as a sleep activity
*/
public int wakeupMinutes;
/**
* Number of times the user woke up
*/
public int wakeupCount;
public HPlusDataRecordSleep(byte[] data) {
super(data, TYPE_SLEEP);
int year = (data[2] & 0xFF) * 256 + (data[1] & 0xFF);
int month = data[3] & 0xFF;
int day = data[4] & 0xFF;
if (year < 2000) //Attempt to recover from bug from device.
year += 1900;
if (year < 2000 || month > 12 || day <= 0 || day > 31) {
throw new IllegalArgumentException("Invalid record date: " + year + "-" + month + "-" + day);
}
enterSleepMinutes = ((data[6] & 0xFF) * 256 + (data[5] & 0xFF));
spindleMinutes = ((data[8] & 0xFF) * 256 + (data[7] & 0xFF));
deepSleepMinutes = ((data[10] & 0xFF) * 256 + (data[9] & 0xFF));
remSleepMinutes = ((data[12] & 0xFF) * 256 + (data[11] & 0xFF));
wakeupMinutes = ((data[14] & 0xFF) * 256 + (data[13] & 0xFF));
wakeupCount = ((data[16] & 0xFF) * 256 + (data[15] & 0xFF));
int hour = data[17] & 0xFF;
int minute = data[18] & 0xFF;
Calendar sleepStart = GregorianCalendar.getInstance();
sleepStart.clear();
sleepStart.set(Calendar.YEAR, year);
sleepStart.set(Calendar.MONTH, month - 1);
sleepStart.set(Calendar.DAY_OF_MONTH, day);
sleepStart.set(Calendar.HOUR_OF_DAY, hour);
sleepStart.set(Calendar.MINUTE, minute);
sleepStart.set(Calendar.SECOND, 0);
sleepStart.set(Calendar.MILLISECOND, 0);
bedTimeStart = (int) (sleepStart.getTimeInMillis() / 1000);
bedTimeEnd = (enterSleepMinutes + spindleMinutes + deepSleepMinutes + remSleepMinutes + wakeupMinutes) * 60 + bedTimeStart;
lightSleepMinutes = enterSleepMinutes + spindleMinutes + remSleepMinutes;
timestamp = bedTimeStart;
}
public List<RecordInterval> getIntervals() {
List<RecordInterval> intervals = new ArrayList<>();
int ts = bedTimeStart + lightSleepMinutes * 60;
intervals.add(new RecordInterval(bedTimeStart, ts, ActivityKind.TYPE_LIGHT_SLEEP));
intervals.add(new RecordInterval(ts, bedTimeEnd, ActivityKind.TYPE_DEEP_SLEEP));
return intervals;
}
public String toString(){
Calendar s = GregorianCalendar.getInstance();
s.setTimeInMillis(bedTimeStart * 1000L);
Calendar end = GregorianCalendar.getInstance();
end.setTimeInMillis(bedTimeEnd * 1000L);
return String.format(Locale.US, "Sleep start: %s end: %s enter: %d spindles: %d rem: %d deep: %d wake: %d-%d", s.getTime(), end.getTime(), enterSleepMinutes, spindleMinutes, remSleepMinutes, deepSleepMinutes, wakeupMinutes, wakeupCount);
}
}

View File

@ -0,0 +1,547 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus;
/*
* @author João Paulo Barraca &lt;jpbarraca@gmail.com&gt;
*/
import android.content.Context;
import android.content.Intent;
import android.support.v4.content.LocalBroadcastManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.GregorianCalendar;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusHealthSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivityOverlay;
import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivityOverlayDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivitySample;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
class HPlusHandlerThread extends GBDeviceIoThread {
private static final Logger LOG = LoggerFactory.getLogger(HPlusHandlerThread.class);
private int CURRENT_DAY_SYNC_PERIOD = 24 * 60 * 60 * 365; //Never
private int CURRENT_DAY_SYNC_RETRY_PERIOD = 10;
private int SLEEP_SYNC_PERIOD = 12 * 60 * 60;
private int SLEEP_SYNC_RETRY_PERIOD = 30;
private int DAY_SUMMARY_SYNC_PERIOD = 24 * 60 * 60;
private int DAY_SUMMARY_SYNC_RETRY_PERIOD = 30;
private boolean mQuit = false;
private HPlusSupport mHPlusSupport;
private int mLastSlotReceived = -1;
private int mLastSlotRequested = 0;
private Calendar mLastSleepDayReceived = GregorianCalendar.getInstance();
private Calendar mGetDaySlotsTime = GregorianCalendar.getInstance();
private Calendar mGetSleepTime = GregorianCalendar.getInstance();
private Calendar mGetDaySummaryTime = GregorianCalendar.getInstance();
private boolean mSlotsInitialSync = true;
private HPlusDataRecordRealtime prevRealTimeRecord = null;
private final Object waitObject = new Object();
List<HPlusDataRecordDaySlot> mDaySlotRecords = new ArrayList<>();
private HPlusDataRecordDaySlot mCurrentDaySlot = null;
public HPlusHandlerThread(GBDevice gbDevice, Context context, HPlusSupport hplusSupport) {
super(gbDevice, context);
mQuit = false;
mHPlusSupport = hplusSupport;
}
@Override
public void run() {
mQuit = false;
sync();
long waitTime = 0;
while (!mQuit) {
if (waitTime > 0) {
synchronized (waitObject) {
try {
waitObject.wait(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
if (mQuit) {
break;
}
if(!mHPlusSupport.getDevice().isConnected()){
quit();
break;
}
Calendar now = GregorianCalendar.getInstance();
if (now.compareTo(mGetDaySlotsTime) > 0) {
requestNextDaySlots();
}
if (now.compareTo(mGetSleepTime) > 0) {
requestNextSleepData();
}
if(now.compareTo(mGetDaySummaryTime) > 0) {
requestDaySummaryData();
}
now = GregorianCalendar.getInstance();
waitTime = Math.min(mGetDaySummaryTime.getTimeInMillis(), Math.min(mGetDaySlotsTime.getTimeInMillis(), mGetSleepTime.getTimeInMillis())) - now.getTimeInMillis();
}
}
@Override
public void quit() {
mQuit = true;
synchronized (waitObject) {
waitObject.notify();
}
}
public void sync() {
mGetSleepTime.setTimeInMillis(0);
mGetDaySlotsTime.setTimeInMillis(0);
mGetDaySummaryTime.setTimeInMillis(0);
mLastSleepDayReceived.setTimeInMillis(0);
mSlotsInitialSync = true;
mLastSlotReceived = -1;
mLastSlotRequested = 0;
mCurrentDaySlot = null;
mDaySlotRecords.clear();
TransactionBuilder builder = new TransactionBuilder("startSyncDayStats");
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_DEVICE_ID});
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_VERSION});
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_CURR_DATA});
builder.queue(mHPlusSupport.getQueue());
synchronized (waitObject) {
waitObject.notify();
}
}
/**
* Process a message containing information regarding a day slot
* A slot summarizes 10 minutes of data
*
* @param data the message from the device
* @return boolean indicating success or fail
*/
public boolean processIncomingDaySlotData(byte[] data) {
HPlusDataRecordDaySlot record;
try{
record = new HPlusDataRecordDaySlot(data);
} catch(IllegalArgumentException e){
LOG.debug((e.getMessage()));
return false;
}
Calendar now = GregorianCalendar.getInstance();
int nowSlot = now.get(Calendar.HOUR_OF_DAY) * 6 + (now.get(Calendar.MINUTE) / 10);
if(record.slot == nowSlot){
if(mCurrentDaySlot != null && mCurrentDaySlot != record){
mCurrentDaySlot.accumulate(record);
mDaySlotRecords.add(mCurrentDaySlot);
mCurrentDaySlot = null;
}else{
//Store it to a temp variable as this is an intermediate value
mCurrentDaySlot = record;
if(!mSlotsInitialSync)
return true;
}
}
if(mSlotsInitialSync) {
//If the slot is in the future, actually it is from the previous day
//Subtract a day of seconds
if(record.slot > nowSlot){
record.timestamp -= 3600 * 24;
}
if (record.slot == mLastSlotReceived + 1) {
mLastSlotReceived = record.slot;
}
//Ignore the current slot as it is incomplete
if(record.slot != nowSlot)
mDaySlotRecords.add(record);
//Still fetching ring buffer. Request the next slots
if (record.slot == mLastSlotRequested) {
mGetDaySlotsTime.clear();
synchronized (waitObject) {
waitObject.notify();
}
}
//Keep buffering
if(record.slot != 143)
return true;
} else {
mGetDaySlotsTime = GregorianCalendar.getInstance();
mGetDaySlotsTime.add(Calendar.DAY_OF_MONTH, 1);
}
if(mDaySlotRecords.size() > 0) {
//Sort the samples
Collections.sort(mDaySlotRecords, new Comparator<HPlusDataRecordDaySlot>() {
public int compare(HPlusDataRecordDaySlot one, HPlusDataRecordDaySlot other) {
return one.timestamp - other.timestamp;
}
});
try (DBHandler dbHandler = GBApplication.acquireDB()) {
HPlusHealthSampleProvider provider = new HPlusHealthSampleProvider(getDevice(), dbHandler.getDaoSession());
List<HPlusHealthActivitySample> samples = new ArrayList<>();
for (HPlusDataRecordDaySlot storedRecord : mDaySlotRecords) {
HPlusHealthActivitySample sample = createSample(dbHandler, storedRecord.timestamp);
sample.setRawHPlusHealthData(storedRecord.getRawData());
sample.setSteps(storedRecord.steps);
sample.setHeartRate(storedRecord.heartRate);
sample.setRawKind(storedRecord.type);
sample.setProvider(provider);
samples.add(sample);
}
provider.getSampleDao().insertOrReplaceInTx(samples);
mDaySlotRecords.clear();
} catch (GBException ex) {
LOG.debug((ex.getMessage()));
} catch (Exception ex) {
LOG.debug(ex.getMessage());
}
}
return true;
}
/**
* Process sleep data from the device
* Devices send a single sleep message for each sleep period
* This message contains the duration of the sub-intervals (rem, deep, etc...)
*
* @param data the message from the device
* @return boolean indicating success or fail
*/
public boolean processIncomingSleepData(byte[] data){
HPlusDataRecordSleep record;
try{
record = new HPlusDataRecordSleep(data);
} catch(IllegalArgumentException e){
LOG.debug((e.getMessage()));
return false;
}
mLastSleepDayReceived.setTimeInMillis(record.bedTimeStart * 1000L);
try (DBHandler dbHandler = GBApplication.acquireDB()) {
DaoSession session = dbHandler.getDaoSession();
Long userId = DBHelper.getUser(session).getId();
Long deviceId = DBHelper.getDevice(getDevice(), session).getId();
HPlusHealthActivityOverlayDao overlayDao = session.getHPlusHealthActivityOverlayDao();
HPlusHealthSampleProvider provider = new HPlusHealthSampleProvider(getDevice(), dbHandler.getDaoSession());
//Get the individual Sleep overlays and insert them
List<HPlusHealthActivityOverlay> overlayList = new ArrayList<>();
List<HPlusDataRecord.RecordInterval> intervals = record.getIntervals();
for(HPlusDataRecord.RecordInterval interval : intervals){
overlayList.add(new HPlusHealthActivityOverlay(interval.timestampFrom, interval.timestampTo, interval.activityKind, deviceId, userId, null));
}
overlayDao.insertOrReplaceInTx(overlayList);
//Store the data
HPlusHealthActivitySample sample = createSample(dbHandler, record.timestamp);
sample.setRawHPlusHealthData(record.getRawData());
sample.setRawKind(record.activityKind);
sample.setProvider(provider);
provider.addGBActivitySample(sample);
} catch (Exception ex) {
LOG.debug(ex.getMessage());
}
mGetSleepTime = GregorianCalendar.getInstance();
mGetSleepTime.add(GregorianCalendar.SECOND, SLEEP_SYNC_PERIOD);
return true;
}
/**
* Process a message containing real time information
*
* @param data the message from the device
* @return boolean indicating success or fail
*/
public boolean processRealtimeStats(byte[] data) {
HPlusDataRecordRealtime record;
try{
record = new HPlusDataRecordRealtime(data);
} catch(IllegalArgumentException e){
LOG.debug((e.getMessage()));
return false;
}
//Skip duplicated messages as the device seems to send the same record multiple times
//This can be used to detect the user is moving (not sleeping)
if(prevRealTimeRecord != null && record.same(prevRealTimeRecord))
return true;
prevRealTimeRecord = record;
getDevice().setBatteryLevel(record.battery);
//Skip when measuring heart rate
//Calories and Distance are updated and these values will be lost.
//Because a message with a valid Heart Rate will be provided, this loss very limited
if(record.heartRate == ActivityKind.TYPE_NOT_MEASURED) {
getDevice().setFirmwareVersion2("---");
getDevice().sendDeviceUpdateIntent(getContext());
}else {
getDevice().setFirmwareVersion2("" + record.heartRate);
getDevice().sendDeviceUpdateIntent(getContext());
}
try (DBHandler dbHandler = GBApplication.acquireDB()) {
HPlusHealthSampleProvider provider = new HPlusHealthSampleProvider(getDevice(), dbHandler.getDaoSession());
HPlusHealthActivitySample sample = createSample(dbHandler, record.timestamp);
sample.setRawKind(record.type);
sample.setRawIntensity(record.intensity);
sample.setHeartRate(record.heartRate);
sample.setDistance(record.distance);
sample.setCalories(record.calories);
sample.setSteps(record.steps);
sample.setRawHPlusHealthData(record.getRawData());
sample.setProvider(provider);
provider.addGBActivitySample(sample);
sample.setSteps(sample.getSteps() - prevRealTimeRecord.steps);
Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES)
.putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample)
.putExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis());
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
//TODO: Handle Active Time. With Overlay?
} catch (GBException ex) {
LOG.debug((ex.getMessage()));
} catch (Exception ex) {
LOG.debug(ex.getMessage());
}
return true;
}
/**
* Process a day summary message
* This message includes aggregates regarding an entire day
*
* @param data the message from the device
* @return boolean indicating success or fail
*/
public boolean processDaySummary(byte[] data) {
HPlusDataRecordDaySummary record;
try{
record = new HPlusDataRecordDaySummary(data);
} catch(IllegalArgumentException e){
LOG.debug((e.getMessage()));
return false;
}
try (DBHandler dbHandler = GBApplication.acquireDB()) {
HPlusHealthSampleProvider provider = new HPlusHealthSampleProvider(getDevice(), dbHandler.getDaoSession());
HPlusHealthActivitySample sample = createSample(dbHandler, record.timestamp);
sample.setRawKind(record.type);
sample.setSteps(record.steps);
sample.setDistance(record.distance);
sample.setCalories(record.calories);
sample.setDistance(record.distance);
sample.setHeartRate((record.maxHeartRate - record.minHeartRate) / 2); //TODO: Find an alternative approach for Day Summary Heart Rate
sample.setRawHPlusHealthData(record.getRawData());
sample.setProvider(provider);
provider.addGBActivitySample(sample);
} catch (GBException ex) {
LOG.debug((ex.getMessage()));
} catch (Exception ex) {
LOG.debug(ex.getMessage());
}
mGetDaySummaryTime = GregorianCalendar.getInstance();
mGetDaySummaryTime.add(Calendar.SECOND, DAY_SUMMARY_SYNC_PERIOD);
return true;
}
/**
* Process a message containing information regarding firmware version
*
* @param data the message from the device
* @return boolean indicating success or fail
*/
public boolean processVersion(byte[] data) {
int major = data[2] & 0xFF;
int minor = data[1] & 0xFF;
getDevice().setFirmwareVersion(major + "." + minor);
getDevice().sendDeviceUpdateIntent(getContext());
return true;
}
/**
* Issue a message requesting the next batch of sleep data
*/
private void requestNextSleepData() {
TransactionBuilder builder = new TransactionBuilder("requestSleepStats");
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_SLEEP});
builder.queue(mHPlusSupport.getQueue());
mGetSleepTime = GregorianCalendar.getInstance();
mGetSleepTime.add(GregorianCalendar.SECOND, SLEEP_SYNC_RETRY_PERIOD);
}
/**
* Issue a message requesting the next set of slots
* The process will sync 1h at a time until the device is in sync
* Then it will request samples until the end of the day in order to minimize data loss
* Messages will be provided every 10 minutes after they are available
*/
private void requestNextDaySlots() {
Calendar now = GregorianCalendar.getInstance();
int currentSlot = now.get(Calendar.HOUR_OF_DAY) * 6 + now.get(Calendar.MINUTE) / 10;
//Finished dumping the entire ring buffer
//Sync to current time
mGetDaySlotsTime = now;
if(mSlotsInitialSync) {
if(mLastSlotReceived == 143) {
mSlotsInitialSync = false;
mGetDaySlotsTime.set(Calendar.SECOND, CURRENT_DAY_SYNC_PERIOD); //Sync complete. Delay timer forever
mLastSlotReceived = -1;
mLastSlotRequested = mLastSlotReceived + 1;
return;
}else {
mGetDaySlotsTime.add(Calendar.SECOND, CURRENT_DAY_SYNC_RETRY_PERIOD);
}
}else{
//Sync complete. Delay timer forever
mGetDaySlotsTime.set(Calendar.SECOND, CURRENT_DAY_SYNC_PERIOD);
return;
}
if(mLastSlotReceived == 143)
mLastSlotReceived = -1;
byte hour = (byte) ((mLastSlotReceived + 1)/ 6);
byte minute = (byte) (((mLastSlotReceived + 1) % 6) * 10);
byte nextHour = hour;
byte nextMinute = 59;
mLastSlotRequested = nextHour * 6 + (nextMinute / 10);
byte[] msg = new byte[]{HPlusConstants.CMD_GET_ACTIVE_DAY, hour, minute, nextHour, nextMinute};
TransactionBuilder builder = new TransactionBuilder("getNextDaySlot");
builder.write(mHPlusSupport.ctrlCharacteristic, msg);
builder.queue(mHPlusSupport.getQueue());
}
/**
* Request a batch of data with the summary of the previous days
*/
public void requestDaySummaryData(){
TransactionBuilder builder = new TransactionBuilder("startSyncDaySummary");
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_DAY_DATA});
builder.queue(mHPlusSupport.getQueue());
mGetDaySummaryTime = GregorianCalendar.getInstance();
mGetDaySummaryTime.add(Calendar.SECOND, DAY_SUMMARY_SYNC_RETRY_PERIOD);
}
/**
* Helper function to create a sample
* @param dbHandler The database handler
* @param timestamp The sample timestamp
* @return The sample just created
*/
private HPlusHealthActivitySample createSample(DBHandler dbHandler, int timestamp){
Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId();
Long deviceId = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId();
HPlusHealthActivitySample sample = new HPlusHealthActivitySample(
timestamp, // ts
deviceId, userId, // User id
null, // Raw Data
ActivityKind.TYPE_UNKNOWN,
0, // Intensity
ActivitySample.NOT_MEASURED, // Steps
ActivitySample.NOT_MEASURED, // HR
ActivitySample.NOT_MEASURED, // Distance
ActivitySample.NOT_MEASURED // Calories
);
return sample;
}
}

View File

@ -1,86 +0,0 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Calendar;
public class HPlusSleepRecord {
private long bedTimeStart;
private long bedTimeEnd;
private int deepSleepSeconds;
private int spindleSeconds;
private int remSleepSeconds;
private int wakeupTime;
private int wakeupCount;
private int enterSleepSeconds;
private byte[] rawData;
HPlusSleepRecord(byte[] data) {
rawData = data;
int year = data[2] * 256 + data[1];
int month = data[3];
int day = data[4];
enterSleepSeconds = data[6] * 256 + data[5];
spindleSeconds = data[8] * 256 + data[7];
deepSleepSeconds = data[10] * 256 + data[9];
remSleepSeconds = data[12] * 256 + data[11];
wakeupTime = data[14] * 256 + data[13];
wakeupCount = data[16] * 256 + data[15];
int hour = data[17];
int minute = data[18];
Calendar c = Calendar.getInstance();
c.set(Calendar.YEAR, year);
c.set(Calendar.MONTH, month);
c.set(Calendar.DAY_OF_MONTH, day);
c.set(Calendar.HOUR, hour);
c.set(Calendar.MINUTE, minute);
c.set(Calendar.SECOND, 0);
c.set(Calendar.MILLISECOND, 0);
bedTimeStart = (c.getTimeInMillis() / 1000L);
bedTimeEnd = bedTimeStart + enterSleepSeconds + spindleSeconds + deepSleepSeconds + remSleepSeconds + wakeupTime;
}
byte[] getRawData() {
return rawData;
}
public long getBedTimeStart() {
return bedTimeStart;
}
public long getBedTimeEnd() {
return bedTimeEnd;
}
public int getDeepSleepSeconds() {
return deepSleepSeconds;
}
public int getSpindleSeconds() {
return spindleSeconds;
}
public int getRemSleepSeconds() {
return remSleepSeconds;
}
public int getWakeupTime() {
return wakeupTime;
}
public int getWakeupCount() {
return wakeupCount;
}
public int getEnterSleepSeconds() {
return enterSleepSeconds;
}
}

View File

@ -545,6 +545,11 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
performPreferredNotification(origin + " received", origin, null);
}
@Override
public void onDeleteNotification(int id) {
}
@Override
public void onSetTime() {
try {

View File

@ -594,6 +594,11 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
performPreferredNotification(origin + " received", origin, alertLevel, null);
}
@Override
public void onDeleteNotification(int id) {
}
@Override
public void onSetTime() {
try {

View File

@ -3,16 +3,25 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble;
import android.util.Pair;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Map;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
class AppMessageHandler {
final PebbleProtocol mPebbleProtocol;
final UUID mUUID;
Map<String, Integer> messageKeys;
AppMessageHandler(UUID uuid, PebbleProtocol pebbleProtocol) {
mUUID = uuid;
@ -28,7 +37,10 @@ class AppMessageHandler {
}
public GBDeviceEvent[] handleMessage(ArrayList<Pair<Integer, Object>> pairs) {
return null;
// Just ACK
GBDeviceEventSendBytes sendBytesAck = new GBDeviceEventSendBytes();
sendBytesAck.encodedBytes = mPebbleProtocol.encodeApplicationMessageAck(mUUID, mPebbleProtocol.last_id);
return new GBDeviceEvent[]{sendBytesAck};
}
public GBDeviceEvent[] onAppStart() {
@ -42,4 +54,15 @@ class AppMessageHandler {
protected GBDevice getDevice() {
return mPebbleProtocol.getDevice();
}
JSONObject getAppKeys() throws IOException, JSONException {
File destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache");
File configurationFile = new File(destDir, mUUID.toString() + ".json");
if (configurationFile.exists()) {
String jsonstring = FileUtils.getStringFromFile(configurationFile);
JSONObject json = new JSONObject(jsonstring);
return json.getJSONObject("appKeys");
}
throw new IOException();
}
}

View File

@ -1,7 +1,12 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble;
import android.util.Pair;
import android.widget.Toast;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.UUID;
@ -10,16 +15,26 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes;
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
class AppMessageHandlerHealthify extends AppMessageHandler {
private static final int KEY_TEMPERATURE = 10021;
private static final int KEY_CONDITIONS = 10022;
private Integer KEY_TEMPERATURE;
private Integer KEY_CONDITIONS;
AppMessageHandlerHealthify(UUID uuid, PebbleProtocol pebbleProtocol) {
super(uuid, pebbleProtocol);
try {
JSONObject appKeys = getAppKeys();
KEY_TEMPERATURE = appKeys.getInt("TEMPERATURE");
KEY_CONDITIONS = appKeys.getInt("CONDITIONS");
} catch (JSONException e) {
GB.toast("There was an error accessing the Helthify watchface configuration.", Toast.LENGTH_LONG, GB.ERROR);
} catch (IOException ignore) {
}
}
private byte[] encodeMarioWeatherMessage(WeatherSpec weatherSpec) {
private byte[] encodeHelthifyWeatherMessage(WeatherSpec weatherSpec) {
if (weatherSpec == null) {
return null;
}
@ -36,14 +51,6 @@ class AppMessageHandlerHealthify extends AppMessageHandler {
return buf.array();
}
@Override
public GBDeviceEvent[] handleMessage(ArrayList<Pair<Integer, Object>> pairs) {
// Just ACK
GBDeviceEventSendBytes sendBytesAck = new GBDeviceEventSendBytes();
sendBytesAck.encodedBytes = mPebbleProtocol.encodeApplicationMessageAck(mUUID, mPebbleProtocol.last_id);
return new GBDeviceEvent[]{sendBytesAck};
}
@Override
public GBDeviceEvent[] onAppStart() {
WeatherSpec weatherSpec = Weather.getInstance().getWeatherSpec();
@ -51,12 +58,12 @@ class AppMessageHandlerHealthify extends AppMessageHandler {
return new GBDeviceEvent[]{null};
}
GBDeviceEventSendBytes sendBytes = new GBDeviceEventSendBytes();
sendBytes.encodedBytes = encodeMarioWeatherMessage(weatherSpec);
sendBytes.encodedBytes = encodeHelthifyWeatherMessage(weatherSpec);
return new GBDeviceEvent[]{sendBytes};
}
@Override
public byte[] encodeUpdateWeather(WeatherSpec weatherSpec) {
return encodeMarioWeatherMessage(weatherSpec);
return encodeHelthifyWeatherMessage(weatherSpec);
}
}

View File

@ -37,14 +37,6 @@ class AppMessageHandlerMarioTime extends AppMessageHandler {
return buf.array();
}
@Override
public GBDeviceEvent[] handleMessage(ArrayList<Pair<Integer, Object>> pairs) {
// Just ACK
GBDeviceEventSendBytes sendBytesAck = new GBDeviceEventSendBytes();
sendBytesAck.encodedBytes = mPebbleProtocol.encodeApplicationMessageAck(mUUID, mPebbleProtocol.last_id);
return new GBDeviceEvent[]{sendBytesAck};
}
@Override
public GBDeviceEvent[] onAppStart() {
WeatherSpec weatherSpec = Weather.getInstance().getWeatherSpec();

View File

@ -1,11 +1,16 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble;
import android.util.Pair;
import android.widget.Toast;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Objects;
import java.util.SimpleTimeZone;
import java.util.TimeZone;
import java.util.UUID;
@ -18,31 +23,21 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSleepMonitorResult;
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleMorpheuzSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.PebbleMorpheuzSample;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
class AppMessageHandlerMorpheuz extends AppMessageHandler {
private static final int KEY_POINT = 1;
private static final int KEY_POINT_46 = 10000;
private static final int KEY_CTRL = 2;
private static final int KEY_CTRL_46 = 10001;
private static final int KEY_FROM = 3;
private static final int KEY_FROM_46 = 10002;
private static final int KEY_TO = 4;
private static final int KEY_TO_46 = 10003;
private static final int KEY_BASE = 5;
private static final int KEY_BASE_46 = 10004;
private static final int KEY_VERSION = 6;
private static final int KEY_VERSION_46 = 10005;
private static final int KEY_GONEOFF = 7;
private static final int KEY_GONEOFF_46 = 10006;
private static final int KEY_TRANSMIT = 8;
private static final int KEY_TRANSMIT_46 = 10007;
private static final int KEY_AUTO_RESET = 9;
private static final int KEY_AUTO_RESET_46 = 10008;
private static final int KEY_SNOOZES = 10;
private static final int KEY_SNOOZES_46 = 10009;
private static final int KEY_FAULT_46 = 10010;
private Integer keyPoint;
private Integer keyCtrl;
private Integer keyFrom;
private Integer keyTo;
private Integer keyBase;
private Integer keyVersion;
private Integer keyGoneoff;
private Integer keyTransmit;
private Integer keyAutoReset;
private Integer keySnoozes;
private Integer keyFault;
private static final int CTRL_TRANSMIT_DONE = 1;
private static final int CTRL_VERSION_DONE = 2;
@ -53,7 +48,6 @@ class AppMessageHandlerMorpheuz extends AppMessageHandler {
private static final int CTRL_SNOOZES_DONE = 64;
// data received from Morpheuz in native format
private int version = 0;
private int smartalarm_from = -1; // time in minutes relative from 0:00 for smart alarm (earliest)
private int smartalarm_to = -1;// time in minutes relative from 0:00 for smart alarm (latest)
private int recording_base_timestamp = -1; // timestamp for the first "point", all folowing are +10 minutes offset each
@ -63,6 +57,24 @@ class AppMessageHandlerMorpheuz extends AppMessageHandler {
public AppMessageHandlerMorpheuz(UUID uuid, PebbleProtocol pebbleProtocol) {
super(uuid, pebbleProtocol);
try {
JSONObject appKeys = getAppKeys();
keyPoint = appKeys.getInt("keyPoint");
keyCtrl = appKeys.getInt("keyCtrl");
keyFrom = appKeys.getInt("keyFrom");
keyTo = appKeys.getInt("keyTo");
keyBase = appKeys.getInt("keyBase");
keyVersion = appKeys.getInt("keyVersion");
keyGoneoff = appKeys.getInt("keyGoneoff");
keyTransmit = appKeys.getInt("keyTransmit");
keyAutoReset = appKeys.getInt("keyAutoReset");
keySnoozes = appKeys.getInt("keySnoozes");
keyFault = appKeys.getInt("keyFault");
} catch (JSONException e) {
GB.toast("There was an error accessing the morpheuz watchapp configuration.", Toast.LENGTH_LONG, GB.ERROR);
} catch (IOException ignore) {
}
}
private byte[] encodeMorpheuzMessage(int key, int value) {
@ -84,88 +96,66 @@ class AppMessageHandlerMorpheuz extends AppMessageHandler {
GBDeviceEventSleepMonitorResult sleepMonitorResult = null;
for (Pair<Integer, Object> pair : pairs) {
switch (pair.first) {
case KEY_TRANSMIT:
case KEY_TRANSMIT_46:
sleepMonitorResult = new GBDeviceEventSleepMonitorResult();
sleepMonitorResult.smartalarm_from = smartalarm_from;
sleepMonitorResult.smartalarm_to = smartalarm_to;
sleepMonitorResult.alarm_gone_off = alarm_gone_off;
sleepMonitorResult.recording_base_timestamp = recording_base_timestamp;
ctrl_message |= CTRL_TRANSMIT_DONE;
break;
case KEY_GONEOFF:
case KEY_GONEOFF_46:
alarm_gone_off = (int) pair.second;
LOG.info("got gone off: " + alarm_gone_off / 60 + ":" + alarm_gone_off % 60);
ctrl_message |= CTRL_DO_NEXT | CTRL_GONEOFF_DONE;
break;
case KEY_POINT:
case KEY_POINT_46:
if (recording_base_timestamp == -1) {
// we have no base timestamp but received points, stop this
ctrl_message = CTRL_VERSION_DONE | CTRL_GONEOFF_DONE | CTRL_TRANSMIT_DONE | CTRL_SET_LAST_SENT;
} else {
int index = ((int) pair.second >> 16);
int intensity = ((int) pair.second & 0xffff);
LOG.info("got point:" + index + " " + intensity);
if (index >= 0) {
try (DBHandler db = GBApplication.acquireDB()) {
Long userId = DBHelper.getUser(db.getDaoSession()).getId();
Long deviceId = DBHelper.getDevice(getDevice(), db.getDaoSession()).getId();
PebbleMorpheuzSampleProvider sampleProvider = new PebbleMorpheuzSampleProvider(getDevice(), db.getDaoSession());
PebbleMorpheuzSample sample = new PebbleMorpheuzSample(recording_base_timestamp + index * 600, deviceId, userId, intensity);
sample.setProvider(sampleProvider);
sampleProvider.addGBActivitySample(sample);
} catch (Exception e) {
LOG.error("Error acquiring database", e);
}
if (Objects.equals(pair.first, keyTransmit)) {
sleepMonitorResult = new GBDeviceEventSleepMonitorResult();
sleepMonitorResult.smartalarm_from = smartalarm_from;
sleepMonitorResult.smartalarm_to = smartalarm_to;
sleepMonitorResult.alarm_gone_off = alarm_gone_off;
sleepMonitorResult.recording_base_timestamp = recording_base_timestamp;
ctrl_message |= CTRL_TRANSMIT_DONE;
} else if (pair.first.equals(keyGoneoff)) {
alarm_gone_off = (int) pair.second;
LOG.info("got gone off: " + alarm_gone_off / 60 + ":" + alarm_gone_off % 60);
ctrl_message |= CTRL_DO_NEXT | CTRL_GONEOFF_DONE;
} else if (pair.first.equals(keyPoint)) {
if (recording_base_timestamp == -1) {
// we have no base timestamp but received points, stop this
ctrl_message = CTRL_VERSION_DONE | CTRL_GONEOFF_DONE | CTRL_TRANSMIT_DONE | CTRL_SET_LAST_SENT;
} else {
int index = ((int) pair.second >> 16);
int intensity = ((int) pair.second & 0xffff);
LOG.info("got point:" + index + " " + intensity);
if (index >= 0) {
try (DBHandler db = GBApplication.acquireDB()) {
Long userId = DBHelper.getUser(db.getDaoSession()).getId();
Long deviceId = DBHelper.getDevice(getDevice(), db.getDaoSession()).getId();
PebbleMorpheuzSampleProvider sampleProvider = new PebbleMorpheuzSampleProvider(getDevice(), db.getDaoSession());
PebbleMorpheuzSample sample = new PebbleMorpheuzSample(recording_base_timestamp + index * 600, deviceId, userId, intensity);
sample.setProvider(sampleProvider);
sampleProvider.addGBActivitySample(sample);
} catch (Exception e) {
LOG.error("Error acquiring database", e);
}
ctrl_message |= CTRL_SET_LAST_SENT | CTRL_DO_NEXT;
}
break;
case KEY_FROM:
case KEY_FROM_46:
smartalarm_from = (int) pair.second;
LOG.info("got from: " + smartalarm_from / 60 + ":" + smartalarm_from % 60);
ctrl_message |= CTRL_SET_LAST_SENT | CTRL_DO_NEXT;
break;
case KEY_TO:
case KEY_TO_46:
smartalarm_to = (int) pair.second;
LOG.info("got to: " + smartalarm_to / 60 + ":" + smartalarm_to % 60);
ctrl_message |= CTRL_SET_LAST_SENT | CTRL_DO_NEXT;
break;
case KEY_VERSION:
case KEY_VERSION_46:
version = (int) pair.second;
LOG.info("got version: " + ((float) version / 10.0f));
ctrl_message |= CTRL_VERSION_DONE;
break;
case KEY_BASE:
case KEY_BASE_46:
// fix timestamp
TimeZone tz = SimpleTimeZone.getDefault();
recording_base_timestamp = (int) pair.second - (tz.getOffset(System.currentTimeMillis())) / 1000;
LOG.info("got base: " + recording_base_timestamp);
ctrl_message |= CTRL_SET_LAST_SENT | CTRL_DO_NEXT;
break;
case KEY_AUTO_RESET:
case KEY_AUTO_RESET_46:
ctrl_message |= CTRL_SET_LAST_SENT | CTRL_DO_NEXT;
break;
case KEY_SNOOZES:
case KEY_SNOOZES_46:
ctrl_message |= CTRL_SNOOZES_DONE | CTRL_DO_NEXT;
break;
case KEY_FAULT_46:
LOG.info("fault code: " + (int) pair.second);
ctrl_message |= CTRL_DO_NEXT;
break;
default:
LOG.info("unhandled key: " + pair.first);
break;
}
} else if (pair.first.equals(keyFrom)) {
smartalarm_from = (int) pair.second;
LOG.info("got from: " + smartalarm_from / 60 + ":" + smartalarm_from % 60);
ctrl_message |= CTRL_SET_LAST_SENT | CTRL_DO_NEXT;
} else if (pair.first.equals(keyTo)) {
smartalarm_to = (int) pair.second;
LOG.info("got to: " + smartalarm_to / 60 + ":" + smartalarm_to % 60);
ctrl_message |= CTRL_SET_LAST_SENT | CTRL_DO_NEXT;
} else if (pair.first.equals(keyVersion)) {
int version = (int) pair.second;
LOG.info("got version: " + ((float) version / 10.0f));
ctrl_message |= CTRL_VERSION_DONE;
} else if (pair.first.equals(keyBase)) {// fix timestamp
TimeZone tz = SimpleTimeZone.getDefault();
recording_base_timestamp = (int) pair.second - (tz.getOffset(System.currentTimeMillis())) / 1000;
LOG.info("got base: " + recording_base_timestamp);
ctrl_message |= CTRL_SET_LAST_SENT | CTRL_DO_NEXT;
} else if (pair.first.equals(keyAutoReset)) {
ctrl_message |= CTRL_SET_LAST_SENT | CTRL_DO_NEXT;
} else if (pair.first.equals(keySnoozes)) {
ctrl_message |= CTRL_SNOOZES_DONE | CTRL_DO_NEXT;
} else if (pair.first.equals(keyFault)) {
LOG.info("fault code: " + (int) pair.second);
ctrl_message |= CTRL_DO_NEXT;
} else {
LOG.info("unhandled key: " + pair.first);
}
}
@ -177,11 +167,7 @@ class AppMessageHandlerMorpheuz extends AppMessageHandler {
GBDeviceEventSendBytes sendBytesCtrl = null;
if (ctrl_message > 0) {
sendBytesCtrl = new GBDeviceEventSendBytes();
int ctrlkey = KEY_CTRL;
if (version >= 46) {
ctrlkey = KEY_CTRL_46;
}
sendBytesCtrl.encodedBytes = encodeMorpheuzMessage(ctrlkey, ctrl_message);
sendBytesCtrl.encodedBytes = encodeMorpheuzMessage(keyCtrl, ctrl_message);
}
// ctrl and sleep monitor might be null, thats okay

View File

@ -0,0 +1,78 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble;
import android.util.Pair;
import android.widget.Toast;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes;
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
class AppMessageHandlerSquare extends AppMessageHandler {
private int CfgKeyCelsiusTemperature;
private int CfgKeyConditions;
private int CfgKeyWeatherMode;
private int CfgKeyUseCelsius;
private int CfgKeyWeatherLocation;
AppMessageHandlerSquare(UUID uuid, PebbleProtocol pebbleProtocol) {
super(uuid, pebbleProtocol);
try {
JSONObject appKeys = getAppKeys();
CfgKeyCelsiusTemperature = appKeys.getInt("CfgKeyCelsiusTemperature");
CfgKeyConditions = appKeys.getInt("CfgKeyConditions");
CfgKeyWeatherMode = appKeys.getInt("CfgKeyWeatherMode");
CfgKeyUseCelsius = appKeys.getInt("CfgKeyUseCelsius");
CfgKeyWeatherLocation = appKeys.getInt("CfgKeyWeatherLocation");
} catch (JSONException e) {
GB.toast("There was an error accessing the Square watchface configuration.", Toast.LENGTH_LONG, GB.ERROR);
} catch (IOException ignore) {
}
}
private byte[] encodeSquareWeatherMessage(WeatherSpec weatherSpec) {
if (weatherSpec == null) {
return null;
}
ArrayList<Pair<Integer, Object>> pairs = new ArrayList<>(2);
pairs.add(new Pair<>(CfgKeyWeatherMode, (Object) 1));
pairs.add(new Pair<>(CfgKeyConditions, (Object) weatherSpec.currentCondition));
pairs.add(new Pair<>(CfgKeyUseCelsius, (Object) 1));
pairs.add(new Pair<>(CfgKeyCelsiusTemperature, (Object) (weatherSpec.currentTemp - 273)));
pairs.add(new Pair<>(CfgKeyWeatherLocation, (Object) (weatherSpec.location)));
byte[] weatherMessage = mPebbleProtocol.encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, mUUID, pairs);
ByteBuffer buf = ByteBuffer.allocate(weatherMessage.length);
buf.put(weatherMessage);
return buf.array();
}
@Override
public GBDeviceEvent[] onAppStart() {
WeatherSpec weatherSpec = Weather.getInstance().getWeatherSpec();
if (weatherSpec == null) {
return new GBDeviceEvent[]{null};
}
GBDeviceEventSendBytes sendBytes = new GBDeviceEventSendBytes();
sendBytes.encodedBytes = encodeSquareWeatherMessage(weatherSpec);
return new GBDeviceEvent[]{sendBytes};
}
@Override
public byte[] encodeUpdateWeather(WeatherSpec weatherSpec) {
return encodeSquareWeatherMessage(weatherSpec);
}
}

View File

@ -1,23 +1,24 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble;
import android.util.Pair;
import android.widget.Toast;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes;
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
class AppMessageHandlerTimeStylePebble extends AppMessageHandler {
private static final int MESSAGE_KEY_WeatherCondition = 10000;
private static final int MESSAGE_KEY_WeatherForecastCondition = 10002;
private static final int MESSAGE_KEY_WeatherForecastHighTemp = 10003;
private static final int MESSAGE_KEY_WeatherForecastLowTemp = 10004;
private static final int MESSAGE_KEY_WeatherTemperature = 10001;
private static final int MESSAGE_KEY_WeatherUseNightIcon = 10025;
private static final int ICON_CLEAR_DAY = 0;
private static final int ICON_CLEAR_NIGHT = 1;
@ -34,6 +35,28 @@ class AppMessageHandlerTimeStylePebble extends AppMessageHandler {
AppMessageHandlerTimeStylePebble(UUID uuid, PebbleProtocol pebbleProtocol) {
super(uuid, pebbleProtocol);
messageKeys = new HashMap<>();
try {
JSONObject appKeys = getAppKeys();
Iterator<String> appKeysIterator = appKeys.keys();
while (appKeysIterator.hasNext()) {
String current = appKeysIterator.next();
switch (current) {
case "WeatherCondition":
case "WeatherForecastCondition":
case "WeatherForecastHighTemp":
case "WeatherForecastLowTemp":
case "WeatherTemperature":
case "SettingUseMetric":
case "WeatherUseNightIcon":
messageKeys.put(current, appKeys.getInt(current));
break;
}
}
} catch (JSONException e) {
GB.toast("There was an error accessing the timestyle watchface configuration.", Toast.LENGTH_LONG, GB.ERROR);
} catch (IOException ignore) {
}
}
/*
@ -98,25 +121,17 @@ class AppMessageHandlerTimeStylePebble extends AppMessageHandler {
ArrayList<Pair<Integer, Object>> pairs = new ArrayList<>();
boolean isNight = false; //TODO: use the night icons when night
pairs.add(new Pair<>(MESSAGE_KEY_WeatherUseNightIcon, (Object) (isNight ? 1 : 0)));
pairs.add(new Pair<>(MESSAGE_KEY_WeatherTemperature, (Object) (weatherSpec.currentTemp - 273)));
pairs.add(new Pair<>(MESSAGE_KEY_WeatherCondition, (Object) (getIconForConditionCode(weatherSpec.currentConditionCode, isNight))));
pairs.add(new Pair<>(MESSAGE_KEY_WeatherForecastCondition, (Object) (getIconForConditionCode(weatherSpec.tomorrowConditionCode, isNight))));
pairs.add(new Pair<>(MESSAGE_KEY_WeatherForecastHighTemp, (Object) (weatherSpec.todayMaxTemp - 273)));
pairs.add(new Pair<>(MESSAGE_KEY_WeatherForecastLowTemp, (Object) (weatherSpec.todayMinTemp - 273)));
pairs.add(new Pair<>(messageKeys.get("SettingUseMetric"), (Object) 1)); //celsius
pairs.add(new Pair<>(messageKeys.get("WeatherUseNightIcon"), (Object) (isNight ? 1 : 0)));
pairs.add(new Pair<>(messageKeys.get("WeatherTemperature"), (Object) (weatherSpec.currentTemp - 273)));
pairs.add(new Pair<>(messageKeys.get("WeatherCondition"), (Object) (getIconForConditionCode(weatherSpec.currentConditionCode, isNight))));
pairs.add(new Pair<>(messageKeys.get("WeatherForecastCondition"), (Object) (getIconForConditionCode(weatherSpec.tomorrowConditionCode, isNight))));
pairs.add(new Pair<>(messageKeys.get("WeatherForecastHighTemp"), (Object) (weatherSpec.todayMaxTemp - 273)));
pairs.add(new Pair<>(messageKeys.get("WeatherForecastLowTemp"), (Object) (weatherSpec.todayMinTemp - 273)));
return mPebbleProtocol.encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, mUUID, pairs);
}
@Override
public GBDeviceEvent[] handleMessage(ArrayList<Pair<Integer, Object>> pairs) {
// Just ACK
GBDeviceEventSendBytes sendBytesAck = new GBDeviceEventSendBytes();
sendBytesAck.encodedBytes = mPebbleProtocol.encodeApplicationMessageAck(mUUID, mPebbleProtocol.last_id);
return new GBDeviceEvent[]{sendBytesAck};
}
@Override
public GBDeviceEvent[] onAppStart() {
WeatherSpec weatherSpec = Weather.getInstance().getWeatherSpec();

View File

@ -1,7 +1,12 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble;
import android.util.Pair;
import android.widget.Toast;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.UUID;
@ -9,17 +14,31 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes;
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
class AppMessageHandlerTrekVolle extends AppMessageHandler {
private static final int MESSAGE_KEY_WEATHER_TEMPERATURE = 10000;
private static final int MESSAGE_KEY_WEATHER_CONDITIONS = 10001;
private static final int MESSAGE_KEY_WEATHER_ICON = 10002;
private static final int MESSAGE_KEY_WEATHER_TEMPERATURE_MIN = 10024;
private static final int MESSAGE_KEY_WEATHER_TEMPERATURE_MAX = 10025;
private static final int MESSAGE_KEY_WEATHER_LOCATION = 10030;
private Integer MESSAGE_KEY_WEATHER_TEMPERATURE;
private Integer MESSAGE_KEY_WEATHER_CONDITIONS;
private Integer MESSAGE_KEY_WEATHER_ICON;
private Integer MESSAGE_KEY_WEATHER_TEMPERATURE_MIN;
private Integer MESSAGE_KEY_WEATHER_TEMPERATURE_MAX;
private Integer MESSAGE_KEY_WEATHER_LOCATION;
AppMessageHandlerTrekVolle(UUID uuid, PebbleProtocol pebbleProtocol) {
super(uuid, pebbleProtocol);
try {
JSONObject appKeys = getAppKeys();
MESSAGE_KEY_WEATHER_TEMPERATURE = appKeys.getInt("WEATHER_TEMPERATURE");
MESSAGE_KEY_WEATHER_CONDITIONS = appKeys.getInt("WEATHER_CONDITIONS");
MESSAGE_KEY_WEATHER_ICON = appKeys.getInt("WEATHER_ICON");
MESSAGE_KEY_WEATHER_TEMPERATURE_MIN = appKeys.getInt("WEATHER_TEMPERATURE_MIN");
MESSAGE_KEY_WEATHER_TEMPERATURE_MAX = appKeys.getInt("WEATHER_TEMPERATURE_MAX");
MESSAGE_KEY_WEATHER_LOCATION = appKeys.getInt("WEATHER_LOCATION");
} catch (JSONException e) {
GB.toast("There was an error accessing the TrekVolle watchface configuration.", Toast.LENGTH_LONG, GB.ERROR);
} catch (IOException ignore) {
}
}
private int getIconForConditionCode(int conditionCode, boolean isNight) {
@ -59,14 +78,6 @@ class AppMessageHandlerTrekVolle extends AppMessageHandler {
return mPebbleProtocol.encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, mUUID, pairs);
}
@Override
public GBDeviceEvent[] handleMessage(ArrayList<Pair<Integer, Object>> pairs) {
// Just ACK
GBDeviceEventSendBytes sendBytesAck = new GBDeviceEventSendBytes();
sendBytesAck.encodedBytes = mPebbleProtocol.encodeApplicationMessageAck(mUUID, mPebbleProtocol.last_id);
return new GBDeviceEvent[]{sendBytesAck};
}
@Override
public GBDeviceEvent[] onAppStart() {
WeatherSpec weatherSpec = Weather.getInstance().getWeatherSpec();

View File

@ -0,0 +1,82 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble;
import android.util.Pair;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes;
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
class AppMessageHandlerZalewszczak extends AppMessageHandler {
private static final int KEY_ICON = 0;
private static final int KEY_TEMP = 1; //celsius
AppMessageHandlerZalewszczak(UUID uuid, PebbleProtocol pebbleProtocol) {
super(uuid, pebbleProtocol);
}
/*
* converted to JAVA from original JS
*/
private int getIconForConditionCode(int conditionCode) {
if (conditionCode < 300) {
return 7;
} else if (conditionCode < 400) {
return 6;
} else if (conditionCode == 511) {
return 8;
} else if (conditionCode < 600) {
return 6;
} else if (conditionCode < 700) {
return 8;
} else if (conditionCode < 800) {
return 10;
} else if (conditionCode == 800) {
return 1;
} else if (conditionCode == 801) {
return 2;
} else if (conditionCode < 900) {
return 5;
} else {
return 0;
}
}
private byte[] encodeWeatherMessage(WeatherSpec weatherSpec) {
if (weatherSpec == null) {
return null;
}
ArrayList<Pair<Integer, Object>> pairs = new ArrayList<>(2);
pairs.add(new Pair<>(KEY_TEMP, (Object) (Math.round(weatherSpec.currentTemp - 273) + "C")));
pairs.add(new Pair<>(KEY_ICON, (Object) (getIconForConditionCode(weatherSpec.currentConditionCode))));
byte[] weatherMessage = mPebbleProtocol.encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, mUUID, pairs);
ByteBuffer buf = ByteBuffer.allocate(weatherMessage.length);
buf.put(weatherMessage);
return buf.array();
}
@Override
public GBDeviceEvent[] onAppStart() {
WeatherSpec weatherSpec = Weather.getInstance().getWeatherSpec();
if (weatherSpec == null) {
return new GBDeviceEvent[]{null};
}
GBDeviceEventSendBytes sendBytes = new GBDeviceEventSendBytes();
sendBytes.encodedBytes = encodeWeatherMessage(weatherSpec);
return new GBDeviceEvent[]{sendBytes};
}
@Override
public byte[] encodeUpdateWeather(WeatherSpec weatherSpec) {
return encodeWeatherMessage(weatherSpec);
}
}

View File

@ -3,16 +3,12 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.ParcelUuid;
import android.support.v4.content.LocalBroadcastManager;
import org.json.JSONArray;
import org.json.JSONException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -27,7 +23,6 @@ import java.net.InetAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
@ -54,21 +49,11 @@ import nodomain.freeyourgadget.gadgetbridge.util.WebViewSingleton;
class PebbleIoThread extends GBDeviceIoThread {
private static final Logger LOG = LoggerFactory.getLogger(PebbleIoThread.class);
public static final String PEBBLEKIT_ACTION_PEBBLE_CONNECTED = "com.getpebble.action.PEBBLE_CONNECTED";
public static final String PEBBLEKIT_ACTION_PEBBLE_DISCONNECTED = "com.getpebble.action.PEBBLE_DISCONNECTED";
public static final String PEBBLEKIT_ACTION_APP_ACK = "com.getpebble.action.app.ACK";
public static final String PEBBLEKIT_ACTION_APP_NACK = "com.getpebble.action.app.NACK";
public static final String PEBBLEKIT_ACTION_APP_RECEIVE = "com.getpebble.action.app.RECEIVE";
public static final String PEBBLEKIT_ACTION_APP_RECEIVE_ACK = "com.getpebble.action.app.RECEIVE_ACK";
public static final String PEBBLEKIT_ACTION_APP_RECEIVE_NACK = "com.getpebble.action.app.RECEIVE_NACK";
public static final String PEBBLEKIT_ACTION_APP_SEND = "com.getpebble.action.app.SEND";
public static final String PEBBLEKIT_ACTION_APP_START = "com.getpebble.action.app.START";
public static final String PEBBLEKIT_ACTION_APP_STOP = "com.getpebble.action.app.STOP";
private final Prefs prefs = GBApplication.getPrefs();
private final PebbleProtocol mPebbleProtocol;
private final PebbleSupport mPebbleSupport;
private PebbleKitSupport mPebbleKitSupport;
private final boolean mEnablePebblekit;
private boolean mIsTCP = false;
@ -95,66 +80,11 @@ class PebbleIoThread extends GBDeviceIoThread {
private int mBinarySize = -1;
private int mBytesWritten = -1;
private final BroadcastReceiver mPebbleKitReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
LOG.info("Got action: " + action);
UUID uuid;
switch (action) {
case PEBBLEKIT_ACTION_APP_START:
case PEBBLEKIT_ACTION_APP_STOP:
uuid = (UUID) intent.getSerializableExtra("uuid");
if (uuid != null) {
write(mPebbleProtocol.encodeAppStart(uuid, action.equals(PEBBLEKIT_ACTION_APP_START)));
}
break;
case PEBBLEKIT_ACTION_APP_SEND:
int transaction_id = intent.getIntExtra("transaction_id", -1);
uuid = (UUID) intent.getSerializableExtra("uuid");
String jsonString = intent.getStringExtra("msg_data");
LOG.info("json string: " + jsonString);
try {
JSONArray jsonArray = new JSONArray(jsonString);
write(mPebbleProtocol.encodeApplicationMessageFromJSON(uuid, jsonArray));
sendAppMessageAck(transaction_id);
} catch (JSONException e) {
e.printStackTrace();
}
break;
case PEBBLEKIT_ACTION_APP_ACK:
// we do not get a uuid and cannot map a transaction id to it, so we ack in PebbleProtocol early
/*
uuid = (UUID) intent.getSerializableExtra("uuid");
int transaction_id = intent.getIntExtra("transaction_id", -1);
if (transaction_id >= 0 && transaction_id <= 255) {
write(mPebbleProtocol.encodeApplicationMessageAck(uuid, (byte) transaction_id));
} else {
LOG.warn("illegal transacktion id " + transaction_id);
}
*/
break;
}
}
};
private void sendAppMessageJS(GBDeviceEventAppMessage appMessage) {
WebViewSingleton.getorInitWebView(getContext(), gbDevice, appMessage.appUUID);
WebViewSingleton.appMessage(appMessage.message);
}
private void sendAppMessageIntent(GBDeviceEventAppMessage appMessage) {
Intent intent = new Intent();
intent.setAction(PEBBLEKIT_ACTION_APP_RECEIVE);
intent.putExtra("uuid", appMessage.appUUID);
intent.putExtra("msg_data", appMessage.message);
intent.putExtra("transaction_id", appMessage.id);
LOG.info("broadcasting to uuid " + appMessage.appUUID + " transaction id: " + appMessage.id + " JSON: " + appMessage.message);
getContext().sendBroadcast(intent);
}
PebbleIoThread(PebbleSupport pebbleSupport, GBDevice gbDevice, GBDeviceProtocol gbDeviceProtocol, BluetoothAdapter btAdapter, Context context) {
super(gbDevice, context);
mPebbleProtocol = (PebbleProtocol) gbDeviceProtocol;
@ -171,14 +101,6 @@ class PebbleIoThread extends GBDeviceIoThread {
return ret;
}
private void sendAppMessageAck(int transactionId) {
Intent intent = new Intent();
intent.setAction(PEBBLEKIT_ACTION_APP_RECEIVE_ACK);
intent.putExtra("transaction_id", transactionId);
LOG.info("broadcasting ACK (transaction id " + transactionId + ")");
getContext().sendBroadcast(intent);
}
@Override
protected boolean connect() {
String deviceAddress = gbDevice.getAddress();
@ -252,7 +174,7 @@ class PebbleIoThread extends GBDeviceIoThread {
}
byte[] buffer = new byte[8192];
enablePebbleKitReceiver(true);
enablePebbleKitSupport(true);
mQuit = false;
while (!mQuit) {
try {
@ -428,7 +350,7 @@ class PebbleIoThread extends GBDeviceIoThread {
mBtSocket = null;
}
enablePebbleKitReceiver(false);
enablePebbleKitSupport(false);
if (mQuit) {
gbDevice.setState(GBDevice.State.NOT_CONNECTED);
@ -441,25 +363,13 @@ class PebbleIoThread extends GBDeviceIoThread {
gbDevice.sendDeviceUpdateIntent(getContext());
}
private void enablePebbleKitReceiver(boolean enable) {
private void enablePebbleKitSupport(boolean enable) {
if (enable && mEnablePebblekit) {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(PEBBLEKIT_ACTION_APP_ACK);
intentFilter.addAction(PEBBLEKIT_ACTION_APP_NACK);
intentFilter.addAction(PEBBLEKIT_ACTION_APP_SEND);
intentFilter.addAction(PEBBLEKIT_ACTION_APP_START);
intentFilter.addAction(PEBBLEKIT_ACTION_APP_STOP);
try {
getContext().registerReceiver(mPebbleKitReceiver, intentFilter);
} catch (IllegalArgumentException e) {
// ignore
}
mPebbleKitSupport = new PebbleKitSupport(getContext(), PebbleIoThread.this, mPebbleProtocol);
} else {
try {
getContext().unregisterReceiver(mPebbleKitReceiver);
} catch (IllegalArgumentException e) {
// ignore
if (mPebbleKitSupport != null) {
mPebbleKitSupport.close();
mPebbleKitSupport = null;
}
}
}
@ -590,7 +500,9 @@ class PebbleIoThread extends GBDeviceIoThread {
sendAppMessageJS((GBDeviceEventAppMessage) deviceEvent);
if (mEnablePebblekit) {
LOG.info("Got AppMessage event");
sendAppMessageIntent((GBDeviceEventAppMessage) deviceEvent);
if (mPebbleKitSupport != null) {
mPebbleKitSupport.sendAppMessageIntent((GBDeviceEventAppMessage) deviceEvent);
}
}
}

View File

@ -0,0 +1,117 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import org.json.JSONArray;
import org.json.JSONException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppMessage;
class PebbleKitSupport {
//private static final String PEBBLEKIT_ACTION_PEBBLE_CONNECTED = "com.getpebble.action.PEBBLE_CONNECTED";
//private static final String PEBBLEKIT_ACTION_PEBBLE_DISCONNECTED = "com.getpebble.action.PEBBLE_DISCONNECTED";
private static final String PEBBLEKIT_ACTION_APP_ACK = "com.getpebble.action.app.ACK";
private static final String PEBBLEKIT_ACTION_APP_NACK = "com.getpebble.action.app.NACK";
private static final String PEBBLEKIT_ACTION_APP_RECEIVE = "com.getpebble.action.app.RECEIVE";
private static final String PEBBLEKIT_ACTION_APP_RECEIVE_ACK = "com.getpebble.action.app.RECEIVE_ACK";
//private static final String PEBBLEKIT_ACTION_APP_RECEIVE_NACK = "com.getpebble.action.app.RECEIVE_NACK";
private static final String PEBBLEKIT_ACTION_APP_SEND = "com.getpebble.action.app.SEND";
private static final String PEBBLEKIT_ACTION_APP_START = "com.getpebble.action.app.START";
private static final String PEBBLEKIT_ACTION_APP_STOP = "com.getpebble.action.app.STOP";
private static final Logger LOG = LoggerFactory.getLogger(PebbleKitSupport.class);
private final PebbleProtocol mPebbleProtocol;
private final Context mContext;
private final PebbleIoThread mPebbleIoThread;
private final BroadcastReceiver mPebbleKitReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
LOG.info("Got action: " + action);
UUID uuid;
switch (action) {
case PEBBLEKIT_ACTION_APP_START:
case PEBBLEKIT_ACTION_APP_STOP:
uuid = (UUID) intent.getSerializableExtra("uuid");
if (uuid != null) {
mPebbleIoThread.write(mPebbleProtocol.encodeAppStart(uuid, action.equals(PEBBLEKIT_ACTION_APP_START)));
}
break;
case PEBBLEKIT_ACTION_APP_SEND:
int transaction_id = intent.getIntExtra("transaction_id", -1);
uuid = (UUID) intent.getSerializableExtra("uuid");
String jsonString = intent.getStringExtra("msg_data");
LOG.info("json string: " + jsonString);
try {
JSONArray jsonArray = new JSONArray(jsonString);
mPebbleIoThread.write(mPebbleProtocol.encodeApplicationMessageFromJSON(uuid, jsonArray));
if (transaction_id >= 0 && transaction_id <= 255) {
sendAppMessageAck(transaction_id);
}
} catch (JSONException e) {
e.printStackTrace();
}
break;
case PEBBLEKIT_ACTION_APP_ACK:
transaction_id = intent.getIntExtra("transaction_id", -1);
if (transaction_id >= 0 && transaction_id <= 255) {
mPebbleIoThread.write(mPebbleProtocol.encodeApplicationMessageAck(null, (byte) transaction_id));
} else {
LOG.warn("illegal transaction id " + transaction_id);
}
break;
}
}
};
PebbleKitSupport(Context context, PebbleIoThread pebbleIoThread, PebbleProtocol pebbleProtocol) {
mContext = context;
mPebbleIoThread = pebbleIoThread;
mPebbleProtocol = pebbleProtocol;
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(PEBBLEKIT_ACTION_APP_ACK);
intentFilter.addAction(PEBBLEKIT_ACTION_APP_NACK);
intentFilter.addAction(PEBBLEKIT_ACTION_APP_SEND);
intentFilter.addAction(PEBBLEKIT_ACTION_APP_START);
intentFilter.addAction(PEBBLEKIT_ACTION_APP_STOP);
mContext.registerReceiver(mPebbleKitReceiver, intentFilter);
}
void sendAppMessageIntent(GBDeviceEventAppMessage appMessage) {
Intent intent = new Intent();
intent.setAction(PEBBLEKIT_ACTION_APP_RECEIVE);
intent.putExtra("uuid", appMessage.appUUID);
intent.putExtra("msg_data", appMessage.message);
intent.putExtra("transaction_id", appMessage.id);
LOG.info("broadcasting to uuid " + appMessage.appUUID + " transaction id: " + appMessage.id + " JSON: " + appMessage.message);
mContext.sendBroadcast(intent);
}
private void sendAppMessageAck(int transactionId) {
Intent intent = new Intent();
intent.setAction(PEBBLEKIT_ACTION_APP_RECEIVE_ACK);
intent.putExtra("transaction_id", transactionId);
LOG.info("broadcasting ACK (transaction id " + transactionId + ")");
mContext.sendBroadcast(intent);
}
void close() {
try {
mContext.unregisterReceiver(mPebbleKitReceiver);
} catch (IllegalArgumentException ignore) {
}
}
}

View File

@ -354,6 +354,8 @@ public class PebbleProtocol extends GBDeviceProtocol {
public static final UUID UUID_PEBBLE_HEALTH = UUID.fromString("36d8c6ed-4c83-4fa1-a9e2-8f12dc941f8c"); // FIXME: store somewhere else, this is also accessed by other code
public static final UUID UUID_WORKOUT = UUID.fromString("fef82c82-7176-4e22-88de-35a3fc18d43f"); // FIXME: store somewhere else, this is also accessed by other code
public static final UUID UUID_WEATHER = UUID.fromString("61b22bc8-1e29-460d-a236-3fe409a439ff"); // FIXME: store somewhere else, this is also accessed by other code
public static final UUID UUID_NOTIFICATIONS = UUID.fromString("b2cae818-10f8-46df-ad2b-98ad2254a3c1");
private static final UUID UUID_GBPEBBLE = UUID.fromString("61476764-7465-7262-6469-656775527a6c");
private static final UUID UUID_MORPHEUZ = UUID.fromString("5be44f1d-d262-4ea6-aa30-ddbec1e3cab2");
private static final UUID UUID_MISFIT = UUID.fromString("0b73b76a-cd65-4dc2-9585-aaa213320858");
@ -362,6 +364,10 @@ public class PebbleProtocol extends GBDeviceProtocol {
private static final UUID UUID_MARIOTIME = UUID.fromString("43caa750-2896-4f46-94dc-1adbd4bc1ff3");
private static final UUID UUID_HELTHIFY = UUID.fromString("7ee97b2c-95e8-4720-b94e-70fccd905d98");
private static final UUID UUID_TREKVOLLE = UUID.fromString("2da02267-7a19-4e49-9ed1-439d25db14e4");
private static final UUID UUID_SQUARE = UUID.fromString("cb332373-4ee5-4c5c-8912-4f62af2d756c");
private static final UUID UUID_ZALEWSZCZAK_CROWEX = UUID.fromString("a88b3151-2426-43c6-b1d0-9b288b3ec47e");
private static final UUID UUID_ZALEWSZCZAK_FANCY = UUID.fromString("014e17bf-5878-4781-8be1-8ef998cee1ba");
private static final UUID UUID_ZALEWSZCZAK_TALLY = UUID.fromString("abb51965-52e2-440a-b93c-843eeacb697d");
private static final UUID UUID_ZERO = new UUID(0, 0);
@ -380,6 +386,10 @@ public class PebbleProtocol extends GBDeviceProtocol {
mAppMessageHandlers.put(UUID_MARIOTIME, new AppMessageHandlerMarioTime(UUID_MARIOTIME, PebbleProtocol.this));
mAppMessageHandlers.put(UUID_HELTHIFY, new AppMessageHandlerHealthify(UUID_HELTHIFY, PebbleProtocol.this));
mAppMessageHandlers.put(UUID_TREKVOLLE, new AppMessageHandlerTrekVolle(UUID_TREKVOLLE, PebbleProtocol.this));
mAppMessageHandlers.put(UUID_SQUARE, new AppMessageHandlerSquare(UUID_SQUARE, PebbleProtocol.this));
mAppMessageHandlers.put(UUID_ZALEWSZCZAK_CROWEX, new AppMessageHandlerZalewszczak(UUID_ZALEWSZCZAK_CROWEX, PebbleProtocol.this));
mAppMessageHandlers.put(UUID_ZALEWSZCZAK_FANCY, new AppMessageHandlerZalewszczak(UUID_ZALEWSZCZAK_FANCY, PebbleProtocol.this));
mAppMessageHandlers.put(UUID_ZALEWSZCZAK_TALLY, new AppMessageHandlerZalewszczak(UUID_ZALEWSZCZAK_TALLY, PebbleProtocol.this));
}
private final HashMap<Byte, DatalogSession> mDatalogSessions = new HashMap<>();
@ -473,6 +483,11 @@ public class PebbleProtocol extends GBDeviceProtocol {
}
}
@Override
public byte[] encodeDeleteNotification(int id) {
return encodeBlobdb(new UUID(GB_UUID_MASK, id), BLOBDB_DELETE, BLOBDB_NOTIFICATION, null);
}
@Override
public byte[] encodeAddCalendarEvent(CalendarEventSpec calendarEventSpec) {
long id = calendarEventSpec.id != -1 ? calendarEventSpec.id : mRandom.nextLong();
@ -907,19 +922,16 @@ public class PebbleProtocol extends GBDeviceProtocol {
}
}
UUID uuid = UUID.randomUUID();
short pin_length = (short) (NOTIFICATION_PIN_LENGTH + attributes_length);
ByteBuffer buf = ByteBuffer.allocate(pin_length);
// pin - 46 bytes
buf.order(ByteOrder.BIG_ENDIAN);
buf.putLong(uuid.getMostSignificantBits());
buf.putInt((int) (uuid.getLeastSignificantBits() >>> 32));
buf.putInt(id);
buf.putLong(uuid.getMostSignificantBits());
buf.putInt((int) (uuid.getLeastSignificantBits() >>> 32));
buf.putInt(id);
buf.putLong(GB_UUID_MASK);
buf.putLong(id);
buf.putLong(UUID_NOTIFICATIONS.getMostSignificantBits());
buf.putLong(UUID_NOTIFICATIONS.getLeastSignificantBits());
buf.order(ByteOrder.LITTLE_ENDIAN);
buf.putInt(timestamp); // 32-bit timestamp
buf.putShort((short) 0); // duration
@ -1686,6 +1698,9 @@ public class PebbleProtocol extends GBDeviceProtocol {
}
byte[] encodeApplicationMessageAck(UUID uuid, byte id) {
if (uuid == null) {
uuid = currentRunningApp;
}
ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + 18); // +ACK
buf.order(ByteOrder.BIG_ENDIAN);
@ -1694,7 +1709,7 @@ public class PebbleProtocol extends GBDeviceProtocol {
buf.put(APPLICATIONMESSAGE_ACK);
buf.put(id);
buf.putLong(uuid.getMostSignificantBits());
buf.putLong(uuid.getMostSignificantBits());
buf.putLong(uuid.getLeastSignificantBits());
return buf.array();
}
@ -1771,6 +1786,9 @@ public class PebbleProtocol extends GBDeviceProtocol {
byte type = buf.get();
short length = buf.getShort();
jsonObject.put("key", key);
if (type == TYPE_CSTRING) {
length--;
}
jsonObject.put("length", length);
switch (type) {
case TYPE_UINT:
@ -1799,10 +1817,11 @@ public class PebbleProtocol extends GBDeviceProtocol {
buf.get(bytes);
if (type == TYPE_BYTEARRAY) {
jsonObject.put("type", "bytes");
jsonObject.put("value", Base64.encode(bytes, Base64.NO_WRAP));
jsonObject.put("value", new String(Base64.encode(bytes, Base64.NO_WRAP)));
} else {
jsonObject.put("type", "string");
jsonObject.put("value", new String(bytes));
buf.get(); // skip null-termination;
}
break;
default:
@ -1813,19 +1832,22 @@ public class PebbleProtocol extends GBDeviceProtocol {
}
// this is a hack we send an ack to the Pebble immediately because we cannot map the transaction_id from the intent back to a uuid yet
/*
GBDeviceEventSendBytes sendBytesAck = new GBDeviceEventSendBytes();
sendBytesAck.encodedBytes = encodeApplicationMessageAck(uuid, last_id);
*/
GBDeviceEventAppMessage appMessage = new GBDeviceEventAppMessage();
appMessage.appUUID = uuid;
appMessage.id = last_id & 0xff;
appMessage.message = jsonArray.toString();
return new GBDeviceEvent[]{appMessage, sendBytesAck};
return new GBDeviceEvent[]{appMessage};
}
byte[] encodeApplicationMessagePush(short endpoint, UUID uuid, ArrayList<Pair<Integer, Object>> pairs) {
int length = LENGTH_UUID + 3; // UUID + (PUSH + id + length of dict)
for (Pair<Integer, Object> pair : pairs) {
if (pair.first == null || pair.second == null)
continue;
length += 7; // key + type + length
if (pair.second instanceof Integer) {
length += 4;
@ -1854,6 +1876,8 @@ public class PebbleProtocol extends GBDeviceProtocol {
buf.order(ByteOrder.LITTLE_ENDIAN);
for (Pair<Integer, Object> pair : pairs) {
if (pair.first == null || pair.second == null)
continue;
buf.putInt(pair.first);
if (pair.second instanceof Integer) {
buf.put(TYPE_INT);
@ -1890,8 +1914,8 @@ public class PebbleProtocol extends GBDeviceProtocol {
try {
JSONObject jsonObject = (JSONObject) jsonArray.get(i);
String type = (String) jsonObject.get("type");
int key = (int) jsonObject.get("key");
int length = (int) jsonObject.get("length");
int key = jsonObject.getInt("key");
int length = jsonObject.getInt("length");
switch (type) {
case "uint":
case "int":

View File

@ -11,6 +11,7 @@ import java.util.ArrayList;
import java.util.Iterator;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
@ -120,7 +121,9 @@ public class PebbleSupport extends AbstractSerialDeviceSupport {
@Override
public void onSetCallState(CallSpec callSpec) {
if (reconnect()) {
super.onSetCallState(callSpec);
if ((callSpec.command != CallSpec.CALL_OUTGOING) || GBApplication.getPrefs().getBoolean("pebble_enable_outgoing_call", true)) {
super.onSetCallState(callSpec);
}
}
}

View File

@ -111,6 +111,11 @@ class PebbleGATTServer extends BluetoothGattServerCallback {
int serial = header >> 3;
if (command == 0x01) {
LOG.info("got ACK for serial = " + serial);
if (mPebbleLESupport.mPPAck != null) {
mPebbleLESupport.mPPAck.countDown();
} else {
LOG.warn("mPPAck countdownlatch is not present but it probably should");
}
}
if (command == 0x02) { // some request?
LOG.info("got command 0x02");

View File

@ -9,6 +9,7 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.concurrent.CountDownLatch;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
@ -23,6 +24,7 @@ public class PebbleLESupport {
private int mMTU = 20;
private int mMTULimit = Integer.MAX_VALUE;
boolean mIsConnected = false;
public CountDownLatch mPPAck;
public PebbleLESupport(Context context, final BluetoothDevice btDevice, PipedInputStream pipedInputStream, PipedOutputStream pipedOutputStream) throws IOException {
mBtDevice = btDevice;
@ -135,6 +137,7 @@ public class PebbleLESupport {
int payloadToSend = bytesRead + 4;
int srcPos = 0;
mPPAck = new CountDownLatch(1);
while (payloadToSend > 0) {
int chunkSize = (payloadToSend < (mMTU - 4)) ? payloadToSend : mMTU - 4;
byte[] outBuf = new byte[chunkSize + 1];
@ -145,7 +148,9 @@ public class PebbleLESupport {
payloadToSend -= chunkSize;
}
Thread.sleep(500); // FIXME ugly wait 0.5s after each pebble package send to the pebble (we do not wait for the GATT chunks)
mPPAck.await();
mPPAck = null;
} catch (IOException | InterruptedException e) {
LOG.info(e.getMessage());
Thread.currentThread().interrupt();

View File

@ -124,6 +124,11 @@ public class VibratissimoSupport extends AbstractBTLEDeviceSupport {
public void onNotification(NotificationSpec notificationSpec) {
}
@Override
public void onDeleteNotification(int id) {
}
@Override
public void onSetTime() {

View File

@ -108,6 +108,12 @@ public abstract class AbstractSerialDeviceSupport extends AbstractDeviceSupport
sendToDevice(bytes);
}
@Override
public void onDeleteNotification(int id) {
byte[] bytes = gbDeviceProtocol.encodeDeleteNotification(id);
sendToDevice(bytes);
}
@Override
public void onSetTime() {
byte[] bytes = gbDeviceProtocol.encodeSetTime();

View File

@ -21,6 +21,10 @@ public abstract class GBDeviceProtocol {
return null;
}
public byte[] encodeDeleteNotification(int id) {
return null;
}
public byte[] encodeSetTime() {
return null;
}

View File

@ -1,7 +1,10 @@
package nodomain.freeyourgadget.gadgetbridge.util;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.os.ParcelUuid;
import android.os.Parcelable;
import android.support.v4.content.LocalBroadcastManager;
public class AndroidUtils {
public static ParcelUuid[] toParcelUUids(Parcelable[] uuids) {
@ -12,4 +15,34 @@ public class AndroidUtils {
System.arraycopy(uuids, 0, uuids2, 0, uuids.length);
return uuids2;
}
/**
* Unregisters the given receiver from the given context.
* @param context the context from which to unregister
* @param receiver the receiver to unregister
* @return true if it was successfully unregistered, or false if the receiver was not registered
*/
public static boolean safeUnregisterBroadcastReceiver(Context context, BroadcastReceiver receiver) {
try {
context.unregisterReceiver(receiver);
return true;
} catch (IllegalArgumentException ex) {
return false;
}
}
/**
* Unregisters the given receiver from the given {@link LocalBroadcastManager}.
* @param manager the manager from which to unregister
* @param receiver the receiver to unregister
* @return true if it was successfully unregistered, or false if the receiver was not registered
*/
public static boolean safeUnregisterBroadcastReceiver(LocalBroadcastManager manager, BroadcastReceiver receiver) {
try {
manager.unregisterReceiver(receiver);
return true;
} catch (IllegalArgumentException ex) {
return false;
}
}
}

View File

@ -23,6 +23,7 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.UnknownDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.MakibesF68Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.liveview.LiveviewCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
@ -123,7 +124,10 @@ public class DeviceHelper {
public GBDevice toSupportedDevice(BluetoothDevice device) {
GBDeviceCandidate candidate = new GBDeviceCandidate(device, GBDevice.RSSI_UNKNOWN, device.getUuids());
return toSupportedDevice(candidate);
}
public GBDevice toSupportedDevice(GBDeviceCandidate candidate) {
for (DeviceCoordinator coordinator : getAllCoordinators()) {
if (coordinator.supports(candidate)) {
return coordinator.createDevice(candidate);
@ -169,6 +173,8 @@ public class DeviceHelper {
result.add(new VibratissimoCoordinator());
result.add(new LiveviewCoordinator());
result.add(new HPlusCoordinator());
result.add(new MakibesF68Coordinator());
return result;
}

View File

@ -65,6 +65,9 @@ public class GB {
if (GBApplication.isRunningLollipopOrLater()) {
builder.setVisibility(Notification.VISIBILITY_PUBLIC);
}
if (GBApplication.minimizeNotification()) {
builder.setPriority(Notification.PRIORITY_MIN);
}
return builder.build();
}

View File

@ -0,0 +1,73 @@
package nodomain.freeyourgadget.gadgetbridge.util;
import java.util.HashMap;
import java.util.Map;
import java.text.Normalizer;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
public class LanguageUtils {
//transliteration map with english equivalent for unsupported chars
private static Map<Character, String> transliterateMap = new HashMap<Character, String>(){
{
//extended ASCII characters
put('æ', "ae"); put('œ', "oe"); put('ß', "B"); put('ª', "a"); put('º', "o"); put('«',"\""); put('»',"\"");
//russian chars
put('а', "a"); put('б', "b"); put('в', "v"); put('г', "g"); put('д', "d"); put('е', "e"); put('ё', "jo"); put('ж', "zh");
put('з', "z"); put('и', "i"); put('й', "jj"); put('к', "k"); put('л', "l"); put('м', "m"); put('н', "n"); put('о', "o");
put('п', "p"); put('р', "r"); put('с', "s"); put('т', "t"); put('у', "u"); put('ф', "f"); put('х', "kh"); put('ц', "c");
put('ч', "ch");put('ш', "sh");put('щ', "shh");put('ъ', "\"");put('ы', "y"); put('ь', "'"); put('э', "eh"); put('ю', "ju");
put('я', "ja");
//continue for other languages...
}
};
//check transliterate option status
public static boolean transliterate()
{
return GBApplication.getPrefs().getBoolean("transliteration", false);
}
//replace unsupported symbols to english analog
public static String transliterate(String txt){
if (txt == null || txt.isEmpty()) {
return txt;
}
StringBuilder message = new StringBuilder();
char[] chars = txt.toCharArray();
for (char c : chars)
{
message.append(transliterate(c));
}
return flattenToAscii(message.toString());
}
//replace unsupported symbol to english analog text
private static String transliterate(char c){
char lowerChar = Character.toLowerCase(c);
if (transliterateMap.containsKey(lowerChar)) {
String replace = transliterateMap.get(lowerChar);
if (lowerChar != c)
{
return replace.toUpperCase();
}
return replace;
}
return String.valueOf(c);
}
//convert diacritic
private static String flattenToAscii(String string) {
string = Normalizer.normalize(string, Normalizer.Form.NFD);
return string.replaceAll("\\p{M}", "");
}
}

View File

@ -0,0 +1,25 @@
package nodomain.freeyourgadget.gadgetbridge.util;
public class StringUtils {
public static String truncate(String s, int maxLength){
int length = Math.min(s.length(), maxLength);
if(length < 0)
return "";
return s.substring(0, length);
}
public static String pad(String s, int length){
return pad(s, length, ' ');
}
public static String pad(String s, int length, char padChar){
while(s.length() < length)
s += padChar;
return s;
}
}

View File

@ -1,17 +1,10 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AbstractAppManagerFragment">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="fill_parent">
<com.woxthebox.draglistview.DragListView
android:id="@+id/appListView"
android:layout_width="match_parent"
android:layout_height="fill_parent" />
</FrameLayout>
</FrameLayout>
</RelativeLayout>

View File

@ -10,7 +10,5 @@
android:descendantFocusability="blocksDescendants"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/alarm_list"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true" />
android:id="@+id/alarm_list" />
</FrameLayout>

View File

@ -2,6 +2,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:paddingBottom="0px"
android:paddingLeft="0px"
android:paddingRight="0px"
@ -28,5 +29,17 @@
</android.support.v4.view.ViewPager>
</LinearLayout>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_gravity="bottom|end"
android:src="@drawable/ic_add_white"
app:elevation="6dp"
app:pressedTranslationZ="12dp"
android:layout_marginBottom="10dp"
android:layout_marginRight="10dp" />
</android.widget.RelativeLayout>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_margin="10dp">
<TextView
android:text="Weather on your Pebble"
android:textAlignment="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textStyle="bold"
android:textSize="20dp"/>
<android.support.v4.widget.Space
android:layout_width="match_parent"
android:layout_height="@dimen/activity_vertical_margin" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/weather_notification_label"
android:text="@string/weather_notification_label"
/>
</LinearLayout>

View File

@ -27,6 +27,9 @@
<item
android:id="@+id/appmanager_weather_deactivate"
android:title="@string/appmanager_weather_deactivate" />
<item
android:id="@+id/appmanager_weather_install_provider"
android:title="@string/appmanager_weather_install_provider" />
<item
android:id="@+id/appmanager_app_configure"
android:title="@string/app_configure"/>

View File

@ -12,7 +12,7 @@
<string name="controlcenter_disconnect">Desconectar</string>
<string name="controlcenter_delete_device">Borrar Dispositivo</string>
<string name="controlcenter_delete_device_name">Borrar %1$s</string>
<string name="controlcenter_delete_device_dialogmessage">¡Esta acción borrará el dispositivo y toda la información asociada a él!</string>
<string name="controlcenter_delete_device_dialogmessage">¡Esta acción borrará el dispositivo y toda su información asociada!</string>
<string name="title_activity_debug">Depuración</string>
<!--Strings related to AppManager-->
<string name="title_activity_appmanager">Gestor de app</string>
@ -27,6 +27,9 @@
<string name="appmanager_health_deactivate">Desactivar</string>
<string name="appmanager_hrm_activate">Activar Monitor de Ritmo Cardíaco</string>
<string name="appmanager_hrm_deactivate">Desactivar Monitor de Ritmo Cardíaco</string>
<string name="appmanager_weather_activate">Activar la aplicación del tiempo del sistema</string>
<string name="appmanager_weather_deactivate">Desactivar la aplicación del tiempo del sistema</string>
<string name="appmanager_weather_install_provider">Instalar la aplicación de notificación del tiempo</string>
<string name="app_configure">Configurar</string>
<string name="app_move_to_top">Mover a la parte de arriba</string>
<!--Strings related to AppBlacklist-->
@ -36,14 +39,14 @@
<string name="fw_upgrade_notice">Estás a punto de instalar el firmware %s en lugar del que está en tu MiBand.</string>
<string name="fw_multi_upgrade_notice">Estás a punto de instalar los firmwares %1$s y %2$s en lugar de los que están en tu MiBand.</string>
<string name="miband_firmware_known">Este firmware ha sido probado y se sabe que es compatible con Gadgetbridge.</string>
<string name="miband_firmware_unknown_warning">Este firmware no ha sido probado y puede que no sea compatible con Gadgetbridge.\n\nNO se recomienda la instalación en tu MiBand!.</string>
<string name="miband_firmware_suggest_whitelist">Si aún así quieres seguir y las cosas continúan funcionando correctamente después de esto, por favor indícales a los desarrolladores de Gadgetbridge que la versión del firmware: %s funciona bien.</string>
<string name="miband_firmware_unknown_warning">Este firmware no ha sido probado y puede que no sea compatible con Gadgetbridge.\n\n¡NO se recomienda la instalación en tu MiBand!.</string>
<string name="miband_firmware_suggest_whitelist">Si aun así quieres seguir y las cosas continúan funcionando correctamente, por favor indícales a los desarrolladores de Gadgetbridge que esta versión del firmware funciona bien: %s .</string>
<!--Strings related to Settings-->
<string name="title_activity_settings">Ajustes</string>
<string name="pref_header_general">Ajustes generales</string>
<string name="pref_title_general_autoconnectonbluetooth">Conectarse al dispositivo cuando el Bluetooth esté activado</string>
<string name="pref_title_general_autocreonnect">Reconectar automáticamente</string>
<string name="pref_title_audo_player">Reproductor de audio preferido</string>
<string name="pref_title_audo_player">Reproductor de audio favorito</string>
<string name="pref_default">Predeterminado</string>
<string name="pref_header_datetime">Fecha y hora</string>
<string name="pref_title_datetime_syctimeonconnect">Sincronizar hora</string>
@ -52,6 +55,9 @@
<string name="pref_theme_light">Claro</string>
<string name="pref_theme_dark">Oscuro</string>
<string name="pref_title_language">Idioma</string>
<string name="pref_title_minimize_priority">Ocultar la notificación de Gadgetbridge</string>
<string name="pref_summary_minimize_priority_off">El icono en la barra de estado y la notificación en la pantalla de bloqueo están activos</string>
<string name="pref_summary_minimize_priority_on">El icono en la barra de estado y la notificación en la pantalla de bloqueo están ocultos</string>
<string name="pref_header_notifications">Notificaciones</string>
<string name="pref_title_notifications_repetitions">Repeticiones</string>
<string name="pref_title_notifications_call">Llamadas telefónicas</string>
@ -62,7 +68,9 @@
<string name="pref_title_notifications_generic">Soporte para notificaciones genéricas</string>
<string name="pref_title_whenscreenon">… también con pantalla encendida</string>
<string name="pref_title_notification_filter">No Molestar</string>
<string name="pref_summary_notification_filter">Dejar de enviar Notificaciones no deseadas basándose en el modo No Molestar</string>
<string name="pref_summary_notification_filter">Dejar de enviar notificaciones no deseadas basándose en el modo No Molestar</string>
<string name="pref_title_transliteration">Transcripción</string>
<string name="pref_summary_transliteration">Utilizar en caso de que tu dispositivo no soporte la fuente de tu idioma (de momento solo el Cirílico)</string>
<string name="always">siempre</string>
<string name="when_screen_off">cuando la pantalla está apagada</string>
<string name="never">nunca</string>
@ -80,10 +88,14 @@
<string name="pref_title_pebble_sync_health">Sincronizar con Pebble Health</string>
<string name="pref_title_pebble_sync_misfit">Sincronizar con Misfit</string>
<string name="pref_title_pebble_sync_morpheuz">Sincronizar con Morpheuz</string>
<string name="pref_title_enable_outgoing_call">Soporte para llamadas salientes</string>
<string name="pref_summary_enable_outgoing_call">Desactivar esto evitará que el Pebble 2/LE vibre en llamadas salientes</string>
<string name="pref_title_enable_pebblekit">Permitir el acceso a aplicaciones Android de terceros</string>
<string name="pref_summary_enable_pebblekit">Permitir el soporte experimental para aplicaciones Android que usan PebbleKit</string>
<string name="pref_title_sunrise_sunset">Salida y puesta de Sol</string>
<string name="pref_summary_sunrise_sunset">Enviar las horas de salida y puesta de Sol basándose en la localización a la línea cronológica del Pebble</string>
<string name="pref_title_autoremove_notifications">Eliminar automáticamente las notificaciones rechazadas</string>
<string name="pref_summary_autoremove_notifications">Las notificaciones se borran automáticamente de Pebble cuando son rechazadas en Android</string>
<string name="pref_header_location">Localización</string>
<string name="pref_title_location_aquire">Buscar localización</string>
<string name="pref_title_location_latitude">Latitud</string>
@ -99,7 +111,7 @@
<string name="pref_title_pebble_forcele">Preferir siempre BLE</string>
<string name="pref_summary_pebble_forcele">Usar el soporte experimental de Pebble LE para todos los Pebble en lugar del bluetooth clásico. Requiere vincular \"Pebble LE\" si un Pebble no-LE ha sido vinculado antes.</string>
<string name="pref_title_pebble_mtu_limit">Pebble 2/LE límite de GATT MTU</string>
<string name="pref_summary_pebble_mtu_limit">Si su Pebble 2/Pebble LE no funciona correctamente, pruebe esta opción para limitar el MTU (rango válido 20512)</string>
<string name="pref_summary_pebble_mtu_limit">Si tu Pebble 2/Pebble LE no funciona correctamente, prueba esta opción para limitar el MTU (rango válido 20512)</string>
<string name="pref_title_pebble_enable_applogs">Activar crear registros de la App del Reloj</string>
<string name="pref_summary_pebble_enable_applogs">Producirá registros de las apps del reloj que Gadgetbridge guardará (necesita reconexión)</string>
<string name="pref_title_pebble_reconnect_attempts">Intentos de reconexión</string>
@ -170,7 +182,7 @@
<string name="vibration_profile_medium">Medio</string>
<string name="vibration_profile_long">Largo</string>
<string name="vibration_profile_waterdrop">Muy largo</string>
<string name="vibration_profile_ring">Ring</string>
<string name="vibration_profile_ring">Timbre</string>
<string name="vibration_profile_alarm_clock">Alarma</string>
<string name="miband_prefs_vibration">Vibración</string>
<string name="vibration_try">Probar</string>
@ -208,7 +220,7 @@
<string name="pbw_install_handler_unable_to_install">No se ha podido instalar el fichero: %1$s</string>
<string name="pbw_install_handler_hw_revision_mismatch">No se puede instalar este firmware: no coincide con la revision hardware de tu Pebble.</string>
<string name="installer_activity_wait_while_determining_status">Por favor, espera mientras se determina el estado de la instalación...</string>
<string name="notif_battery_low_title">Batería baja del Gadget!</string>
<string name="notif_battery_low_title">¡Batería baja del Gadget!</string>
<string name="notif_battery_low_percent">A %1$s le queda: %2$s%% batería</string>
<string name="notif_battery_low_bigtext_last_charge_time">Última carga: %s \n</string>
<string name="notif_battery_low_bigtext_number_of_charges">Número de cargas: %s</string>
@ -219,13 +231,13 @@
<string name="fwapp_install_device_not_ready">El archivo no puede ser instalado, el dispositivo no está listo.</string>
<string name="miband_installhandler_miband_firmware">Miband firmware %1$s</string>
<string name="miband_fwinstaller_compatible_version">Versión compatible</string>
<string name="miband_fwinstaller_untested_version">Versión no probada!</string>
<string name="miband_fwinstaller_untested_version">¡Versión no probada!</string>
<string name="fwappinstaller_connection_state">Conexión al dispositivo: %1$s</string>
<string name="pbw_installhandler_pebble_firmware">Pebble firmware %1$s</string>
<string name="pbwinstallhandler_correct_hw_revision">Revisión de hardware correcta</string>
<string name="pbwinstallhandler_incorrect_hw_revision">La revisión de hardware es incorrecta!</string>
<string name="pbwinstallhandler_incorrect_hw_revision">¡La versión de hardware es incorrecta!</string>
<string name="pbwinstallhandler_app_item">%1$s (%2$s)</string>
<string name="updatefirmwareoperation_updateproblem_do_not_reboot">Hubo un problema con la transferencia de firmware. NO REINICIES tu Mi Band!</string>
<string name="updatefirmwareoperation_updateproblem_do_not_reboot">Hubo un problema con la transferencia de firmware. ¡NO REINICIES tu Mi Band!</string>
<string name="updatefirmwareoperation_metadata_updateproblem">Hubo un problema con la transferencia de metadatos del firmware</string>
<string name="updatefirmwareoperation_update_complete">Instalación del firmware completa</string>
<string name="updatefirmwareoperation_update_complete_rebooting">Instalación del firmware completa, reiniciando dispositivo...</string>
@ -284,7 +296,7 @@
<string name="title_activity_onboarding">Importar Base de Datos</string>
<string name="import_old_db_buttonlabel">Importar datos de actividad antiguos</string>
<string name="import_old_db_information">Desde Gadgetbridge 0.12.0 usamos un nuevo formato de base de datos.
Se pueden importar los antiguos datos de actividad y asociarlos con el dispoditivo al que se está conectando (%1$s).\n
Se pueden importar los antiguos datos de actividad y asociarlos con el dispositivo al que se está conectando (%1$s).\n
\n
Si no importas los antiguos datos de actividad ahora, siempre lo podrás hacer después seleccionando \"MERGE OLD ACTIVITY DATA\" en el apartado de gestión de base de datos de actividad.\n
\n
@ -303,17 +315,17 @@ Por favor, ten en cuenta que puedes importar datos desde Mi Band, Pebble Health
<string name="dbmanagementactivity_overwrite_database_confirmation">¿Quiere sobreescribir la base de datos actual? Todos sus datos actuales (si los hay) se borrarán.</string>
<string name="dbmanagementactivity_import_successful">Importado con éxito.</string>
<string name="dbmanagementactivity_error_importing_db">Error importando DB: %1$s</string>
<string name="dbmanagementactivity_no_old_activitydatabase_found">No se ha encontrado una Base de Datos con actividad antigua, no se importará nada.</string>
<string name="dbmanagementactivity_no_connected_device">No hay ningún dispositivo conectado al que asociar la Base de Datos antigua.</string>
<string name="dbmanagementactivity_merging_activity_data_title">Uniendo los datos de actividad</string>
<string name="dbmanagementactivity_no_old_activitydatabase_found">No se ha encontrado una base de datos con actividad antigua, no se importará nada.</string>
<string name="dbmanagementactivity_no_connected_device">No hay ningún dispositivo conectado al que asociar la base de datos antigua.</string>
<string name="dbmanagementactivity_merging_activity_data_title">Fusionando los datos de actividad</string>
<string name="dbmanagementactivity_please_wait_while_merging">Por favor, espere mientras se unen las bases de datos.</string>
<string name="dbmanagementactivity_error_importing_old_activity_data">Error importando los datos antiguos a la nueva Base de Datos.</string>
<string name="dbmanagementactivity_error_importing_old_activity_data">Error importando los datos antiguos a la nueva base de datos.</string>
<string name="dbmanagementactivity_associate_old_data_with_device">Asociar los datos antiguos al dispositivo</string>
<string name="dbmanagementactivity_delete_activity_data_title">¿Quiere borrar los datos de actividad?</string>
<string name="dbmanagementactivity_really_delete_entire_db">¿Quiere borrar la base de datos? Todos sus datos de actividad y la información sobre sus dispositivos se borrarán.</string>
<string name="dbmanagementactivity_delete_activity_data_title">¿Quieres borrar los datos de actividad?</string>
<string name="dbmanagementactivity_really_delete_entire_db">¿Quieres borrar la base de datos? Todos tus datos de actividad y la información sobre tus dispositivos se borrarán.</string>
<string name="dbmanagementactivity_database_successfully_deleted">Datos borrados.</string>
<string name="dbmanagementactivity_db_deletion_failed">El borrado de la Base de Datos ha fallado.</string>
<string name="dbmanagementactivity_delete_old_activity_db">¿Quieres borrar la antigua Base de Datos de Actividades?</string>
<string name="dbmanagementactivity_db_deletion_failed">El borrado de la base de datos ha fallado.</string>
<string name="dbmanagementactivity_delete_old_activity_db">¿Quieres borrar la antigua base de datos de actividades?</string>
<string name="dbmanagementactivity_delete_old_activitydb_confirmation">¿Quiere borrar la base de datos antigua? Si los datos no se han importado, se perderán.</string>
<string name="dbmanagementactivity_old_activity_db_successfully_deleted">Los datos antiguos han sido borrados.</string>
<string name="dbmanagementactivity_old_activity_db_deletion_failed">El borrado de la Base de datos antiguos ha fallado.</string>
@ -324,5 +336,6 @@ Por favor, ten en cuenta que puedes importar datos desde Mi Band, Pebble Health
<string name="title_activity_vibration">Vibración</string>
<!--Strings related to Pebble Pairing Activity-->
<string name="title_activity_pebble_pairing">Emparejando con Pebble</string>
<string name="pebble_pairing_hint">En su dispositivo Android va a aparecer un mensaje para emparejarse. Si no apareciera, mire en el cajón de notificaciones y acepte la propuesta de emparejamiento. Después acepte también en su Pebble.</string>
<string name="pebble_pairing_hint">En su dispositivo Android va a aparecer un mensaje para emparejarse. Si no apareciera, mira en el cajón de notificaciones y acepta la propuesta de emparejamiento. Después acepta también en tu Pebble.</string>
<string name="weather_notification_label">Asegúrate de que este tema esté activado en la aplicación de notificación del tiempo para obtener la información en tu Pebble.\n\nNo se requiere configuración.\n\nPuedes activar la aplicación del tiempo del sistema desde la configuración de la app.\n\nLas watchfaces soportadas mostrarán la información del tiempo automáticamente.</string>
</resources>

View File

@ -22,8 +22,13 @@
<string name="appmananger_app_delete">Cancella</string>
<string name="appmananger_app_delete_cache">Cancella e rimuovi dalla cache</string>
<string name="appmananger_app_reinstall">Re-installazione</string>
<string name="appmanager_app_openinstore">Cerca nell\'appstore di Pebble</string>
<string name="appmanager_health_activate">Attiva</string>
<string name="appmanager_health_deactivate">Disattiva</string>
<string name="appmanager_hrm_activate">Attiva il monitor del battito cardiaco</string>
<string name="appmanager_hrm_deactivate">Disattiva il monitor del battito cardiaco</string>
<string name="appmanager_weather_activate">Attiva l\'applicazione meteo</string>
<string name="appmanager_weather_deactivate">Disattiva l\'applicazione meteo</string>
<string name="app_configure">Configura</string>
<string name="app_move_to_top">Sposta in cima</string>
<!--Strings related to AppBlacklist-->
@ -49,6 +54,9 @@
<string name="pref_theme_light">Chiaro</string>
<string name="pref_theme_dark">Scuro</string>
<string name="pref_title_language">Lingua</string>
<string name="pref_title_minimize_priority">Nascondi la notifica di gadgetbridge</string>
<string name="pref_summary_minimize_priority_off">L\'icona nella barra di stato e la notifica nella schermata di blocco vengono mostrate</string>
<string name="pref_summary_minimize_priority_on">L\'icona nella barra di stato e la notifica nella schermata di blocco non vengono mostrate</string>
<string name="pref_header_notifications">Notifiche</string>
<string name="pref_title_notifications_repetitions">Ripetizioni</string>
<string name="pref_title_notifications_call">Chiamate telefoniche</string>
@ -77,6 +85,8 @@
<string name="pref_title_pebble_sync_health">Sincronizza Pebble Health</string>
<string name="pref_title_pebble_sync_misfit">Sincronizza Misfit</string>
<string name="pref_title_pebble_sync_morpheuz">Sincronizza Morpheuz</string>
<string name="pref_title_enable_outgoing_call">Mostra le chiamate in uscita</string>
<string name="pref_summary_enable_outgoing_call">Disabilitando questa funzionalità impedirá la vibrazione del Pebble 2 quando si effettua una chiamata</string>
<string name="pref_title_enable_pebblekit">Consenti accesso ad altre applicazioni</string>
<string name="pref_summary_enable_pebblekit">Attiva l\'accesso sperimentale ad applicazioni Android che usano PebbleKit</string>
<string name="pref_title_sunrise_sunset">Alba e tramonto</string>
@ -85,12 +95,20 @@
<string name="pref_title_location_aquire">Acquisisci posizione</string>
<string name="pref_title_location_latitude">Latitudine</string>
<string name="pref_title_location_longitude">Longitudine</string>
<string name="pref_title_location_keep_uptodate">Mantieni aggiornata la posizion</string>
<string name="pref_summary_location_keep_uptodate">Cerca di ottenere la posizione aggiornata durante l\'utilizzo, usa quella memorizzata come backup</string>
<string name="toast_enable_networklocationprovider">Per cortesia abilita la localizzazione utilizzando la rete</string>
<string name="toast_aqurired_networklocation">posizione acquisita</string>
<string name="pref_title_pebble_forceprotocol">Forza protocollo delle notifiche</string>
<string name="pref_summary_pebble_forceprotocol">Questa opzione forza l\'utilizzo della versione più recente delle notifiche in dipendenza del firmware del tuo dispositivo. ABILITALO SOLO SE SAI COSA STAI FACENDO!</string>
<string name="pref_title_pebble_forceuntested">Abilita funzionalità non testate</string>
<string name="pref_summary_pebble_forceuntested">Abilita funzionalità non testate. ABILITARE SOLO SE SI SA QUELLO CHE SI STA FACENDO!</string>
<string name="pref_title_pebble_forcele">Usa sempre BLE</string>
<string name="pref_summary_pebble_forcele">Utilizza il supporto sperimentale Pebble BLE per tutti i Pebble. Si devei effettuare il \"pairing\" nuovamente se si dovesse utilizzare di nuovo il BT classico</string>
<string name="pref_title_pebble_mtu_limit">Limita la MTU del Pebble 2/LE</string>
<string name="pref_summary_pebble_mtu_limit">Se il tuo Pebble 2/LE non funziona come dovrebbe, prova a impostare un limite alla MTU (range valido 20-512)</string>
<string name="pref_title_pebble_enable_applogs">Abilita il log delle applicazioni che girano su Pebble</string>
<string name="pref_summary_pebble_enable_applogs">Il log delle applicazioni che girano su Pebble verrà aggiunto a quello di Gadgetbridge (richiede riconessione)</string>
<string name="pref_title_pebble_reconnect_attempts">Tentativi di riconessione</string>
<string name="not_connected">non connesso</string>
<string name="connecting">in collegamento</string>
@ -163,6 +181,9 @@
<string name="pref_screen_notification_profile_generic">Notifiche generiche</string>
<string name="pref_screen_notification_profile_email">Notifiche Email</string>
<string name="pref_screen_notification_profile_incoming_call">Notifiche chiamata in arrivo</string>
<string name="pref_screen_notification_profile_generic_chat">Cha</string>
<string name="pref_screen_notification_profile_generic_navigation">Navigazione</string>
<string name="pref_screen_notification_profile_generic_social">Social Network</string>
<string name="control_center_find_lost_device">Trova dispositivo smarrito</string>
<string name="control_center_cancel_to_stop_vibration">Annulla per fermare la vibrazione</string>
<string name="title_activity_charts">Le tue attività</string>
@ -236,6 +257,8 @@
<string name="miband_prefs_reserve_alarm_calendar">Sveglie da riservare per i prossimi eventi del calendario</string>
<string name="miband_prefs_hr_sleep_detection">Utilizza il sensore del battito cardiaco per migliorare il riconoscimento del sonno</string>
<string name="miband_prefs_device_time_offset_hours">Sfasamento dell\'orario per il device (per consentire il rilevamento del sonno per chi lavora a turni)</string>
<string name="miband2_prefs_dateformat">Mi2: formato della data</string>
<string name="dateformat_time">Ora</string>
<string name="waiting_for_reconnect">in attesa di riconnessione</string>
<string name="activity_prefs_about_you">Informazioni sull\'utilizzatore</string>
<string name="activity_prefs_year_birth">Anno di nascita</string>

View File

@ -27,6 +27,9 @@
<string name="appmanager_health_deactivate">非アクティベート</string>
<string name="appmanager_hrm_activate">HRM をアクティベート</string>
<string name="appmanager_hrm_deactivate">HRM を非アクティベート</string>
<string name="appmanager_weather_activate">システムの天気アプリを有効にする</string>
<string name="appmanager_weather_deactivate">システムの天気アプリを無効にする</string>
<string name="appmanager_weather_install_provider">天気予報アプリをインストール</string>
<string name="app_configure">設定</string>
<string name="app_move_to_top">先頭に移動</string>
<!--Strings related to AppBlacklist-->
@ -52,6 +55,9 @@
<string name="pref_theme_light">ライト</string>
<string name="pref_theme_dark">ダーク</string>
<string name="pref_title_language">言語</string>
<string name="pref_title_minimize_priority">ガジェットブリッジの通知を非表示</string>
<string name="pref_summary_minimize_priority_off">ステータスバーのアイコンとロック画面の通知が表示されます</string>
<string name="pref_summary_minimize_priority_on">ステータスバーのアイコンとロック画面の通知は非表示です</string>
<string name="pref_header_notifications">通知</string>
<string name="pref_title_notifications_repetitions">繰り返し</string>
<string name="pref_title_notifications_call">電話通知</string>
@ -63,6 +69,8 @@
<string name="pref_title_whenscreenon">… スクリーンがオンのときにも</string>
<string name="pref_title_notification_filter">サイレント</string>
<string name="pref_summary_notification_filter">サイレントモードに基づいて、送信される不要な通知を停止します。</string>
<string name="pref_title_transliteration">音訳</string>
<string name="pref_summary_transliteration">デバイスでお使いの言語のフォントがサポートされていない場合に、使用してください (現在はキリル文字のみ)</string>
<string name="always">いつも</string>
<string name="when_screen_off">スクリーンがオフのとき</string>
<string name="never">なし</string>
@ -80,10 +88,14 @@
<string name="pref_title_pebble_sync_health">Pebble Health 同期</string>
<string name="pref_title_pebble_sync_misfit">Misfit 同期</string>
<string name="pref_title_pebble_sync_morpheuz">Morpheuz 同期</string>
<string name="pref_title_enable_outgoing_call">発信のサポート</string>
<string name="pref_summary_enable_outgoing_call">これを無効にすると、Pebble 2/LE は発信時に振動しなくなります</string>
<string name="pref_title_enable_pebblekit">第三者のアンドロイドアップにアクセス権利を与える</string>
<string name="pref_summary_enable_pebblekit">PebbleKitを使用してAndroidアプリ用の実験的なサポートを有効にします</string>
<string name="pref_title_sunrise_sunset">日の出と日の入り</string>
<string name="pref_summary_sunrise_sunset">場所に基づいて、Pebble のタイムラインに日の出・日の入りの時間を送ります</string>
<string name="pref_title_autoremove_notifications">消去した通知を自動削除</string>
<string name="pref_summary_autoremove_notifications">通知は、Androidデバイスから削除されたときに、Pebbleから自動的に削除されます</string>
<string name="pref_header_location">場所</string>
<string name="pref_title_location_aquire">場所の取得</string>
<string name="pref_title_location_latitude">緯度</string>
@ -325,4 +337,5 @@ Mi Band、Pebble Health、Morpheuz からデータをインポートすること
<!--Strings related to Pebble Pairing Activity-->
<string name="title_activity_pebble_pairing">Pebbleペアリング</string>
<string name="pebble_pairing_hint">お使いのAndroidデバイスでペアリングのダイアログがポップアップすると思います。 起こらない場合は、通知ドロワーを調べて、ペアリング要求を受け入れます。 その後、Pebbleでペアリング要求を受け入れます</string>
<string name="weather_notification_label">天気予報アプリでこのスキンが有効になっていることを確認してください。\n\nここで必要な設定はありません。\n\nアプリ管理からPebbleのシステム天気アプリを有効にすることができます。\n\nサポートされているウォッチフェイスが自動的に天気を表示します。</string>
</resources>

View File

@ -52,6 +52,8 @@
<string name="pref_title_whenscreenon">… даже когда экран включён</string>
<string name="pref_title_notification_filter">Не беспокоить</string>
<string name="pref_summary_notification_filter">Предотвращать отправку нежелательных уведомлений в режиме \"Не беспокоить\"</string>
<string name="pref_title_transliteration">Транслитерация</string>
<string name="pref_summary_transliteration">Используйте, если устройство не может работать с вашим языком</string>
<string name="always">всегда</string>
<string name="when_screen_off">когда экран выключен</string>
<string name="never">никогда</string>

View File

@ -32,6 +32,7 @@
<string name="appmanager_hrm_deactivate">Deactivate HRM</string>
<string name="appmanager_weather_activate">Activate system weather app</string>
<string name="appmanager_weather_deactivate">Deactivate system weather app</string>
<string name="appmanager_weather_install_provider">Install the weather notification app</string>
<string name="app_configure">Configure</string>
<string name="app_move_to_top">Move to top</string>
@ -65,6 +66,10 @@
<string name="pref_title_language">Language</string>
<string name="pref_title_minimize_priority">Hide the Gadgetbridge notification</string>
<string name="pref_summary_minimize_priority_off">The icon in the status bar and the notification in the lockscreen are shown</string>
<string name="pref_summary_minimize_priority_on">The icon in the status bar and the notification in the lockscreen are hidden</string>
<string name="pref_header_notifications">Notifications</string>
<string name="pref_title_notifications_repetitions">Repetitions</string>
<string name="pref_title_notifications_call">Phone Calls</string>
@ -76,6 +81,8 @@
<string name="pref_title_whenscreenon">… also when screen is on</string>
<string name="pref_title_notification_filter">Do Not Disturb</string>
<string name="pref_summary_notification_filter">Stop unwanted Notifications from being sent based on the Do Not Disturb mode.</string>
<string name="pref_title_transliteration">Transliteration</string>
<string name="pref_summary_transliteration">Enable this if your device has no support for your language\'s font (Currently Cyrillic only)</string>
<string name="always">always</string>
<string name="when_screen_off">when screen is off</string>
@ -100,12 +107,18 @@
<string name="pref_title_pebble_sync_misfit">Sync Misfit</string>
<string name="pref_title_pebble_sync_morpheuz">Sync Morpheuz</string>
<string name="pref_title_enable_outgoing_call">Support outgoing calls</string>
<string name="pref_summary_enable_outgoing_call">Disabling this will also stop the Pebble 2/LE to vibrate on outgoing calls</string>
<string name="pref_title_enable_pebblekit">Allow 3rd Party Android App Access</string>
<string name="pref_summary_enable_pebblekit">Enable experimental support for Android Apps using PebbleKit</string>
<string name="pref_title_sunrise_sunset">Sunrise and Sunset</string>
<string name="pref_summary_sunrise_sunset">Send sunrise and sunset times based on the location to the pebble timeline</string>
<string name="pref_title_autoremove_notifications">Autoremove dismissed Notifications</string>
<string name="pref_summary_autoremove_notifications">Notifications are automatically removed from the Pebble when dismissed from the Android device</string>
<string name="pref_header_location">Location</string>
<string name="pref_title_location_aquire">Acquire Location</string>
<string name="pref_title_location_latitude">Latitude</string>
@ -365,4 +378,8 @@
<!-- Strings related to Pebble Pairing Activity-->
<string name="title_activity_pebble_pairing">Pebble Pairing</string>
<string name="pebble_pairing_hint">A pairing dialog is expected to pop up on your Android device. If that does not happen, look in the notification drawer and accept the pairing request. After that accept the pairing request on your Pebble</string>
<string name="weather_notification_label">Make sure that this skin is enabled in the Weather Notification app to get weather information on your Pebble.\n\nNo configuration is needed here.\n\nYou can enable the system weather app of your Pebble from the app management.\n\nSupported watchfaces will show the weather automatically.</string>
<string name="pref_title_setup_bt_pairing">Enable Bluetooth pairing</string>
<string name="pref_summary_setup_bt_pairing">Deactivate this if you have trouble connecting</string>
</resources>

View File

@ -1,5 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<changelog>
<release version="0.17.3" versioncode="84">
<change>HPlus: Improve display of new messages and phone calls</change>
<change>HPlus: Fix bug related to steps and heart rate</change>
<change>Pebble: Support dynamic keys for natively supported watchfaces and watchapps (more stability accross versions)</change>
<change>Pebble: Fix error Toast being displayed when TimeStyle watchface is not installed</change>
<change>Mi Band 1+2: Support for connecting wihout BT pairing (workaround for certain connection problems)</change>
</release>
<release version="0.17.2" versioncode="83">
<change>Pebble: Fix temperature unit in Timestyle Pebble watchface</change>
<change>Add optional Cyrillic transliteration (for devices lacking the font)</change>
</release>
<release version="0.17.1" versioncode="82">
<change>Pebble: Fix installation of some watchapps</change>
<change>Pebble: Try to improve PebbleKit compatibility</change>
<change>HPlus: Fix bug setting current date</change>
</release>
<release version="0.17.0" versioncode="81">
<change>Add weather support through "Weather Notification" app</change>
<change>Various fixes for K9 mail when using the generic notification receiver</change>
<change>Add a preference to hide the notification icon of Gadgetbridge</change>
<change>Pebble: Support for build-in weather system app (FW 4.x)</change>
<change>Pebble: Add weather support for various watchfaces</change>
<change>Pebble: Add option to automatically delete notifications that got dismissed on the phone</change>
<change>Pebble: Add option to disable call display</change>
<change>Pebble: Bugfix for some PebbleKit enabled 3rd party apps (TCW and maybe other)</change>
<change>Pebble 2/LE: Improve reliablitly and transfer speed</change>
<change>HPlus: Improved discovery and pairing</change>
<change>HPlus: Improved notifications (display + vibration)</change>
<change>HPlus: Synchronize time and date</change>
<change>HPlus: Display firmware version and battery charge</change>
<change>HPlus: Near real time Heart rate measurement</change>
<change>HPlus: Experimental synchronization of activity data (only sleep, steps and intensity)</change>
<change>HPlus: Fix some disconnection issues</change>
</release>
<release version="0.16.0" versioncode="80">
<change>New devices: HPlus (e.g. Zeblaze ZeBand), contributed by João Paulo Barraca</change>
<change>ZeBand: Initial support: notifications, heart rate, sleep monitoring, user configuration, date+time</change>

View File

@ -275,6 +275,11 @@
<PreferenceCategory
android:key="pref_key_development"
android:title="@string/pref_header_development">
<CheckBoxPreference
android:key="mi_setup_bt_pairing"
android:title="@string/pref_title_setup_bt_pairing"
android:summary="@string/pref_summary_setup_bt_pairing"
android:defaultValue="true" />
<EditTextPreference
android:digits="0123456789ABCDEF:"
android:key="development_miaddr"

View File

@ -30,6 +30,12 @@
android:entryValues="@array/pref_language_values"
android:defaultValue="default"
android:summary="%s" />
<CheckBoxPreference
android:defaultValue="false"
android:key="minimize_priority"
android:summaryOff="@string/pref_summary_minimize_priority_off"
android:summaryOn="@string/pref_summary_minimize_priority_on"
android:title="@string/pref_title_minimize_priority" />
</PreferenceCategory>
<PreferenceCategory
android:key="pref_key_datetime"
@ -85,6 +91,13 @@
android:key="notifications_generic_whenscreenon"
android:title="@string/pref_title_whenscreenon" />
<CheckBoxPreference
android:defaultValue="false"
android:key="transliteration"
android:summary="@string/pref_summary_transliteration"
android:title="@string/pref_title_transliteration"
/>
<CheckBoxPreference
android:defaultValue="false"
android:key="notification_filter"
@ -146,6 +159,11 @@
android:title="@string/pref_title_pebble_settings">
<PreferenceCategory
android:title="@string/pref_header_general">
<CheckBoxPreference
android:defaultValue="true"
android:key="pebble_enable_outgoing_call"
android:summary="@string/pref_summary_enable_outgoing_call"
android:title="@string/pref_title_enable_outgoing_call" />
<CheckBoxPreference
android:defaultValue="false"
android:key="pebble_enable_pebblekit"
@ -161,6 +179,11 @@
android:title="@string/pref_title_sunrise_sunset"
android:summary="@string/pref_summary_sunrise_sunset"
android:key="send_sunrise_sunset" />
<CheckBoxPreference
android:defaultValue="false"
android:key="autoremove_notifications"
android:summary="@string/pref_summary_autoremove_notifications"
android:title="@string/pref_title_autoremove_notifications" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/pref_header_activitytrackers">
<ListPreference

View File

@ -1,17 +1,21 @@
package nodomain.freeyourgadget.gadgetbridge.service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import org.junit.Test;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.Mockito;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.test.TestBase;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_BODY;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@ -97,4 +101,17 @@ public class DeviceCommunicationServiceTestCase extends TestBase {
inOrder.verifyNoMoreInteractions();
}
@Test
public void testTransliterationSupport() {
SharedPreferences settings = GBApplication.getPrefs().getPreferences();
SharedPreferences.Editor editor = settings.edit();
editor.putBoolean("transliteration", true);
editor.commit();
Intent intent = mDeviceService.createIntent().putExtra(EXTRA_NOTIFICATION_BODY, "Прõсто текčт");
mDeviceService.invokeService(intent);
String result = intent.getStringExtra(EXTRA_NOTIFICATION_BODY);
assertTrue("Transliteration support fail!", result.equals("Prosto tekct"));
}
}

View File

@ -29,7 +29,7 @@ class TestDeviceService extends GBDeviceService {
// calling though to the service natively does not work with robolectric,
// we have to use the ServiceController to do that
service.onStartCommand(intent, Service.START_FLAG_REDELIVERY, (int) (Math.random() * 10000));
// super.invokeService(intent);
super.invokeService(intent);
}
@Override

View File

@ -54,6 +54,11 @@ class TestDeviceSupport extends AbstractDeviceSupport {
}
@Override
public void onDeleteNotification(int id) {
}
@Override
public void onSetTime() {

View File

@ -0,0 +1,38 @@
package nodomain.freeyourgadget.gadgetbridge.test;
import android.content.SharedPreferences;
import org.junit.Test;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.util.LanguageUtils;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* Tests LanguageUtils
*/
public class LanguageUtilsTest extends TestBase {
@Test
public void testStringTransliterate() throws Exception {
//input with cyrillic and diacritic letters
String input = "Прõсто текčт";
String output = LanguageUtils.transliterate(input);
String result = "Prosto tekct";
assertTrue(String.format("Transliteration fail! Expected '%s', but found '%s'}", result, output), output.equals(result));
}
@Test
public void testTransliterateOption() throws Exception {
assertFalse("Transliteration option fail! Expected 'Off' by default, but result is 'On'", LanguageUtils.transliterate());
SharedPreferences settings = GBApplication.getPrefs().getPreferences();
SharedPreferences.Editor editor = settings.edit();
editor.putBoolean("transliteration", true);
editor.commit();
assertTrue("Transliteration option fail! Expected 'On', but result is 'Off'", LanguageUtils.transliterate());
}
}

View File

@ -0,0 +1,14 @@
package nodomain.freeyourgadget.gadgetbridge.test;
import org.junit.Assert;
import org.junit.Test;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic;
public class MiscTest extends TestBase {
@Test
public void testGattCharacteristic() {
String desc = GattCharacteristic.lookup(GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT, "xxx");
Assert.assertEquals("heart rate control point", desc);
}
}