From eac8f5f8c7280a3e2896bd2a8aa7cc0d9939f011 Mon Sep 17 00:00:00 2001 From: cpfeiffer Date: Sun, 15 Oct 2017 22:12:37 +0200 Subject: [PATCH 01/17] Extract isValidHeartRate() method to HeartRateUtils --- .../gadgetbridge/activities/HeartRateUtils.java | 4 ++++ .../activities/charts/AbstractChartFragment.java | 6 +----- .../activities/charts/LiveActivityFragment.java | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/HeartRateUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/HeartRateUtils.java index 3c7f9b394..bb9f3d2be 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/HeartRateUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/HeartRateUtils.java @@ -27,4 +27,8 @@ public class HeartRateUtils { * Value is in minutes */ public static final int MAX_HR_MEASUREMENTS_GAP_MINUTES = 10; + + public static boolean isValidHeartRateValue(int value) { + return value > HeartRateUtils.MIN_HEART_RATE_VALUE && value < HeartRateUtils.MAX_HEART_RATE_VALUE; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java index a2b0cb8c0..0abd1b47b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java @@ -486,7 +486,7 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { colors.add(akActivity.color); } activityEntries.add(createBarEntry(value, ts)); - if (hr && isValidHeartRateValue(sample.getHeartRate())) { + if (hr && HeartRateUtils.isValidHeartRateValue(sample.getHeartRate())) { if (lastHrSampleIndex > -1 && ts - lastHrSampleIndex > 1800*HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) { heartrateEntries.add(createLineEntry(0, lastHrSampleIndex + 1)); heartrateEntries.add(createLineEntry(0, ts - 1)); @@ -550,10 +550,6 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { return new DefaultChartsData(combinedData, xValueFormatter); } - protected boolean isValidHeartRateValue(int value) { - return value > HeartRateUtils.MIN_HEART_RATE_VALUE && value < HeartRateUtils.MAX_HEART_RATE_VALUE; - } - /** * Implement this to supply the samples to be displayed. * diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/LiveActivityFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/LiveActivityFragment.java index c91023035..a292dfd4c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/LiveActivityFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/LiveActivityFragment.java @@ -168,7 +168,7 @@ public class LiveActivityFragment extends AbstractChartFragment { private void addSample(ActivitySample sample) { int heartRate = sample.getHeartRate(); int timestamp = tsTranslation.shorten(sample.getTimestamp()); - if (isValidHeartRateValue(heartRate)) { + if (HeartRateUtils.isValidHeartRateValue(heartRate)) { setCurrentHeartRate(heartRate, timestamp); } int steps = sample.getSteps(); From f5318287c4de8edcd4ce3e066b1e9ae5ad94811a Mon Sep 17 00:00:00 2001 From: cpfeiffer Date: Thu, 19 Oct 2017 21:52:38 +0200 Subject: [PATCH 02/17] WIP: Bip GPS fetching, parsing, exporting Also adjust Mi2 date/time sending wrt DST offsets --- .../gadgetbridge/daogen/GBDaoGenerator.java | 30 + app/src/main/AndroidManifest.xml | 6 +- .../gadgetbridge/GBApplication.java | 23 + .../freeyourgadget/gadgetbridge/Logging.java | 2 +- .../activities/AbstractListActivity.java | 51 ++ .../activities/ActivitySummariesActivity.java | 80 ++ .../adapter/AbstractItemAdapter.java | 122 +++ .../adapter/ActivitySummariesAdapter.java | 56 ++ .../adapter/GBDeviceAdapterv2.java | 17 + .../adapter/ItemWithDetailsAdapter.java | 66 +- .../database/UsedConfiguration.java | 32 - .../devices/AbstractDeviceCoordinator.java | 5 + .../devices/DeviceCoordinator.java | 9 +- .../amazfitbip/BipActivitySummary.java | 206 +++++ .../amazfitbip/AmazfitBipCoordinator.java | 5 + .../huami/amazfitbip/AmazfitBipService.java | 3 + .../devices/miband/MiBand2SampleProvider.java | 65 +- .../devices/miband2/MiBand2Const.java | 68 ++ .../export/ActivityTrackExporter.java | 15 + .../gadgetbridge/export/GPXExporter.java | 158 ++++ .../gadgetbridge/model/ActivityKind.java | 69 ++ .../gadgetbridge/model/ActivityPoint.java | 95 ++ .../gadgetbridge/model/ActivitySummary.java | 33 + .../gadgetbridge/model/ActivityTrack.java | 62 ++ .../gadgetbridge/model/GPSCoordinate.java | 65 ++ .../service/btle/BLETypeConversions.java | 41 +- .../gadgetbridge/service/btle/BtLEQueue.java | 6 +- .../amazfitbip/ActivityDetailsParser.java | 216 +++++ .../devices/amazfitbip/AmazfitBipSupport.java | 5 +- .../AmazfitBipFetchLogsOperation.java | 10 +- .../devices/miband2/MiBand2Support.java | 23 +- .../operations/AbstractFetchOperation.java | 10 +- .../operations/FetchActivityOperation.java | 7 +- .../FetchSportsDetailsOperation.java | 174 ++++ .../FetchSportsSummaryOperation.java | 164 +++- .../gadgetbridge/util/AndroidUtils.java | 24 + .../gadgetbridge/util/DateTimeUtils.java | 7 +- .../gadgetbridge/util/FileUtils.java | 10 + .../main/res/drawable/ic_activity_biking.xml | 7 + .../res/drawable/ic_activity_deep_sleep.xml | 7 + .../res/drawable/ic_activity_light_sleep.xml | 7 + .../res/drawable/ic_activity_not_measured.xml | 7 + .../main/res/drawable/ic_activity_running.xml | 7 + .../main/res/drawable/ic_activity_tracks.xml | 7 + .../main/res/drawable/ic_activity_unknown.xml | 7 + .../main/res/drawable/ic_activity_walking.xml | 7 + app/src/main/res/layout/activity_list.xml | 12 + app/src/main/res/layout/device_itemv2.xml | 17 +- app/src/main/res/values/strings.xml | 12 + ...ot_provider_paths.xml => shared_paths.xml} | 2 + .../test/ActivityDetailsParserTest.java | 110 +++ .../test/HexToBinaryInputStream.java | 52 ++ .../test/HexToBinaryInputStreamTest.java | 50 + .../test/resources/ActivityDetailsDump1.txt | 861 ++++++++++++++++++ 54 files changed, 3000 insertions(+), 212 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AbstractListActivity.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesActivity.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/AbstractItemAdapter.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/ActivitySummariesAdapter.java delete mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/UsedConfiguration.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/amazfitbip/BipActivitySummary.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband2/MiBand2Const.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/export/ActivityTrackExporter.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/export/GPXExporter.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityPoint.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummary.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityTrack.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/GPSCoordinate.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/ActivityDetailsParser.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsDetailsOperation.java create mode 100644 app/src/main/res/drawable/ic_activity_biking.xml create mode 100644 app/src/main/res/drawable/ic_activity_deep_sleep.xml create mode 100644 app/src/main/res/drawable/ic_activity_light_sleep.xml create mode 100644 app/src/main/res/drawable/ic_activity_not_measured.xml create mode 100644 app/src/main/res/drawable/ic_activity_running.xml create mode 100644 app/src/main/res/drawable/ic_activity_tracks.xml create mode 100644 app/src/main/res/drawable/ic_activity_unknown.xml create mode 100644 app/src/main/res/drawable/ic_activity_walking.xml create mode 100644 app/src/main/res/layout/activity_list.xml rename app/src/main/res/xml/{screenshot_provider_paths.xml => shared_paths.xml} (63%) create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/ActivityDetailsParserTest.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/HexToBinaryInputStream.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/HexToBinaryInputStreamTest.java create mode 100644 app/src/test/resources/ActivityDetailsDump1.txt diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 1f9c7bdbc..99b2dacbe 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -15,6 +15,8 @@ */ package nodomain.freeyourgadget.gadgetbridge.daogen; +import java.util.Date; + import de.greenrobot.daogenerator.DaoGenerator; import de.greenrobot.daogenerator.Entity; import de.greenrobot.daogenerator.Index; @@ -32,6 +34,7 @@ public class GBDaoGenerator { private static final String MAIN_PACKAGE = "nodomain.freeyourgadget.gadgetbridge"; private static final String MODEL_PACKAGE = MAIN_PACKAGE + ".model"; private static final String VALID_BY_DATE = MODEL_PACKAGE + ".ValidByDate"; + private static final String ACTIVITY_SUMMARY = MODEL_PACKAGE + ".ActivitySummary"; private static final String OVERRIDE = "@Override"; private static final String SAMPLE_RAW_INTENSITY = "rawIntensity"; private static final String SAMPLE_STEPS = "steps"; @@ -68,6 +71,8 @@ public class GBDaoGenerator { addCalendarSyncState(schema, device); + addBipActivitySummary(schema, user, device); + new DaoGenerator().generateAll(schema, "app/src/main/java"); } @@ -297,6 +302,31 @@ public class GBDaoGenerator { calendarSyncState.addIntProperty("hash").notNull(); } + private static void addBipActivitySummary(Schema schema, Entity user, Entity device) { + Entity summary = addEntity(schema, "BaseActivitySummary"); + summary.implementsInterface(ACTIVITY_SUMMARY); + summary.addIdProperty(); + + summary.setJavaDoc( + "This class represents the summary of a user's activity event. I.e. a walk, hike, a bicycle tour, etc."); + + summary.addStringProperty("name").codeBeforeGetter(OVERRIDE); + summary.addDateProperty("startTime").notNull().codeBeforeGetter(OVERRIDE); + summary.addDateProperty("endTime").notNull().codeBeforeGetter(OVERRIDE); + summary.addIntProperty("activityKind").notNull().codeBeforeGetter(OVERRIDE); + + summary.addIntProperty("baseLongitude").javaDocGetterAndSetter("Temporary, bip-specific"); + summary.addIntProperty("baseLatitude").javaDocGetterAndSetter("Temporary, bip-specific"); + summary.addIntProperty("baseAltitude").javaDocGetterAndSetter("Temporary, bip-specific"); + + summary.addStringProperty("gpxTrack").codeBeforeGetter(OVERRIDE); + + Property deviceId = summary.addLongProperty("deviceId").notNull().codeBeforeGetter(OVERRIDE).getProperty(); + summary.addToOne(device, deviceId); + Property userId = summary.addLongProperty("userId").notNull().codeBeforeGetter(OVERRIDE).getProperty(); + summary.addToOne(user, userId); + } + private static Property findProperty(Entity entity, String propertyName) { for (Property prop : entity.getProperties()) { if (propertyName.equals(prop.getPropertyName())) { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 49ae11746..55524bbc4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -54,6 +54,10 @@ android:name=".devices.miband.MiBandPreferencesActivity" android:label="@string/preferences_miband_settings" android:parentActivityName=".activities.SettingsActivity" /> + + android:resource="@xml/shared_paths" /> diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java index 7ee81ed53..26b051be0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java @@ -24,6 +24,9 @@ import android.app.NotificationManager.Policy; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; import android.database.Cursor; @@ -591,4 +594,24 @@ public class GBApplication extends Application { public static Locale getLanguage() { return language; } + + public String getVersion() { + try { + return getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_META_DATA).versionName; + } catch (PackageManager.NameNotFoundException e) { + GB.log("Unable to determine Gadgetbridge's version", GB.WARN, e); + return "0.0.0"; + } + } + + public String getNameAndVersion() { + try { + ApplicationInfo appInfo = getPackageManager().getApplicationInfo(getContext().getPackageName(), PackageManager.GET_META_DATA); + PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_META_DATA); + return String.format("%s %s", appInfo.name, packageInfo.versionName); + } catch (PackageManager.NameNotFoundException e) { + GB.log("Unable to determine Gadgetbridge's name/version", GB.WARN, e); + return "Gadgetbridge"; + } + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Logging.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Logging.java index 30bf3af1f..94604a80b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Logging.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Logging.java @@ -148,7 +148,7 @@ public abstract class Logging { } StringBuilder builder = new StringBuilder(bytes.length * 5); for (byte b : bytes) { - builder.append(String.format("0x%2x", b)); + builder.append(String.format("0x%02x", b)); builder.append(" "); } return builder.toString().trim(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AbstractListActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AbstractListActivity.java new file mode 100644 index 000000000..bbdd11e2a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AbstractListActivity.java @@ -0,0 +1,51 @@ +package nodomain.freeyourgadget.gadgetbridge.activities; + +import android.os.Bundle; +import android.support.v7.widget.Toolbar; +import android.widget.ListView; +import android.widget.Toast; + +import java.util.ArrayList; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.adapter.AbstractItemAdapter; +import nodomain.freeyourgadget.gadgetbridge.adapter.ItemWithDetailsAdapter; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary; +import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; +import nodomain.freeyourgadget.gadgetbridge.model.GenericItem; +import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public abstract class AbstractListActivity extends AbstractGBActivity { + private AbstractItemAdapter itemAdapter; + private ListView itemListView; + + public void setItemAdapter(AbstractItemAdapter itemAdapter) { + this.itemAdapter = itemAdapter; + itemListView.setAdapter(itemAdapter); + } + + protected void refresh() { + this.itemAdapter.loadItems(); + } + + public AbstractItemAdapter getItemAdapter() { + return itemAdapter; + } + + public ListView getItemListView() { + return itemListView; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_list); + itemListView = (ListView) findViewById(R.id.itemListView); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesActivity.java new file mode 100644 index 000000000..721251e81 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesActivity.java @@ -0,0 +1,80 @@ +package nodomain.freeyourgadget.gadgetbridge.activities; + +import android.content.Intent; +import android.os.Bundle; +import android.view.ContextMenu; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.Toast; + +import java.io.IOException; + +import nodomain.freeyourgadget.gadgetbridge.adapter.ActivitySummariesAdapter; +import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummary; +import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class ActivitySummariesActivity extends AbstractListActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setItemAdapter(new ActivitySummariesAdapter(this)); + + getItemListView().setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + Object item = getItemAdapter().getItem(position); + if (item != null) { + ActivitySummary summary = (ActivitySummary) item; + String gpxTrack = summary.getGpxTrack(); + if (gpxTrack != null) { + showTrack(gpxTrack); + } + } + } + }); + + + getItemListView().setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() { + @Override + public void onCreateContextMenu(ContextMenu menu, View v, final ContextMenu.ContextMenuInfo menuInfo) { + MenuItem delete = menu.add("Delete"); + delete.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; + deleteItemAt(info.position); + return true; + } + }); + } + }); + + getItemListView().setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { + @Override + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + return getItemListView().showContextMenu(); + } + }); + } + + private void deleteItemAt(int position) { + BaseActivitySummary item = getItemAdapter().getItem(position); + if (item != null) { + item.delete(); + getItemAdapter().remove(item); + refresh(); + } + } + + private void showTrack(String gpxTrack) { + try { + AndroidUtils.viewFile(gpxTrack, Intent.ACTION_VIEW, this); + } catch (IOException e) { + GB.toast(this, "Unable to display GPX track: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/AbstractItemAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/AbstractItemAdapter.java new file mode 100644 index 000000000..9ad6e57a1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/AbstractItemAdapter.java @@ -0,0 +1,122 @@ +/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.adapter; + +import android.content.Context; +import android.support.annotation.DrawableRes; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.R; + +/** + * Adapter for displaying generic ItemWithDetails instances. + */ +public abstract class AbstractItemAdapter extends ArrayAdapter { + + public static final int SIZE_SMALL = 1; + public static final int SIZE_MEDIUM = 2; + public static final int SIZE_LARGE = 3; + private final Context context; + private final List items; + private boolean horizontalAlignment; + private int size = SIZE_MEDIUM; + + public AbstractItemAdapter(Context context) { + this (context, new ArrayList()); + } + + public AbstractItemAdapter(Context context, List items) { + super(context, 0, items); + + this.context = context; + this.items = items; + } + + public void setHorizontalAlignment(boolean horizontalAlignment) { + this.horizontalAlignment = horizontalAlignment; + } + + @Override + public View getView(int position, View view, ViewGroup parent) { + T item = getItem(position); + + if (view == null) { + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + if (horizontalAlignment) { + view = inflater.inflate(R.layout.item_with_details_horizontal, parent, false); + } else { + switch (size) { + case SIZE_SMALL: + view = inflater.inflate(R.layout.item_with_details_small, parent, false); + break; + default: + view = inflater.inflate(R.layout.item_with_details, parent, false); + break; + } + } + } + ImageView iconView = (ImageView) view.findViewById(R.id.item_image); + TextView nameView = (TextView) view.findViewById(R.id.item_name); + TextView detailsView = (TextView) view.findViewById(R.id.item_details); + + nameView.setText(getName(item)); + detailsView.setText(getDetails(item)); + iconView.setImageResource(getIcon(item)); + + return view; + } + + protected abstract String getName(T item); + + protected abstract String getDetails(T item); + + @DrawableRes + protected abstract int getIcon(T item); + + public void setSize(int size) { + this.size = size; + } + + public int getSize() { + return size; + } + + public List getItems() { + return items; + } + + public void loadItems() { + } + + public void setItems(List items, boolean notify) { + this.items.clear(); + this.items.addAll(items); + if (notify) { + notifyDataSetChanged(); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/ActivitySummariesAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/ActivitySummariesAdapter.java new file mode 100644 index 000000000..c90ff1831 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/ActivitySummariesAdapter.java @@ -0,0 +1,56 @@ +package nodomain.freeyourgadget.gadgetbridge.adapter; + +import android.content.Context; +import android.widget.Toast; + +import java.util.Date; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary; +import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class ActivitySummariesAdapter extends AbstractItemAdapter { + public ActivitySummariesAdapter(Context context) { + super(context); + loadItems(); + } + + public void loadItems() { + try (DBHandler handler = GBApplication.acquireDB()) { + BaseActivitySummaryDao summaryDao = handler.getDaoSession().getBaseActivitySummaryDao(); + List allSummaries = summaryDao.loadAll(); + setItems(allSummaries, true); + } catch (Exception e) { + GB.toast("Error loading activity summaries.", Toast.LENGTH_SHORT, GB.ERROR, e); + } + } + + @Override + protected String getName(BaseActivitySummary item) { + String name = item.getName(); + if (name != null && name.length() > 0) { + return name; + } + + Date startTime = item.getStartTime(); + if (startTime != null) { + return DateTimeUtils.formatDateTime(startTime); + } + return "Unknown activity"; + } + + @Override + protected String getDetails(BaseActivitySummary item) { + return ActivityKind.asString(item.getActivityKind(), getContext()); + } + + @Override + protected int getIcon(BaseActivitySummary item) { + return ActivityKind.getIconId(item.getActivityKind()); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java index b8f6373cc..ffa6178dd 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java @@ -43,6 +43,7 @@ import java.util.List; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.ActivitySummariesActivity; import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureAlarms; import nodomain.freeyourgadget.gadgetbridge.activities.VibrationActivity; import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity; @@ -210,6 +211,20 @@ public class GBDeviceAdapterv2 extends RecyclerView.Adapter { - - public static final int SIZE_SMALL = 1; - public static final int SIZE_MEDIUM = 2; - public static final int SIZE_LARGE = 3; - private final Context context; - private boolean horizontalAlignment; - private int size = SIZE_MEDIUM; +public class ItemWithDetailsAdapter extends AbstractItemAdapter { public ItemWithDetailsAdapter(Context context, List items) { - super(context, 0, items); - - this.context = context; - } - - public void setHorizontalAlignment(boolean horizontalAlignment) { - this.horizontalAlignment = horizontalAlignment; + super(context, items); } @Override - public View getView(int position, View view, ViewGroup parent) { - ItemWithDetails item = getItem(position); - - if (view == null) { - LayoutInflater inflater = (LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - - if (horizontalAlignment) { - view = inflater.inflate(R.layout.item_with_details_horizontal, parent, false); - } else { - switch (size) { - case SIZE_SMALL: - view = inflater.inflate(R.layout.item_with_details_small, parent, false); - break; - default: - view = inflater.inflate(R.layout.item_with_details, parent, false); - break; - } - } - } - ImageView iconView = (ImageView) view.findViewById(R.id.item_image); - TextView nameView = (TextView) view.findViewById(R.id.item_name); - TextView detailsView = (TextView) view.findViewById(R.id.item_details); - - nameView.setText(item.getName()); - detailsView.setText(item.getDetails()); - iconView.setImageResource(item.getIcon()); - - return view; + protected String getName(ItemWithDetails item) { + return item.getName(); } - public void setSize(int size) { - this.size = size; + @Override + protected String getDetails(ItemWithDetails item) { + return item.getDetails(); } - public int getSize() { - return size; + @Override + protected int getIcon(ItemWithDetails item) { + return item.getIcon(); } + } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/UsedConfiguration.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/UsedConfiguration.java deleted file mode 100644 index 01b174365..000000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/UsedConfiguration.java +++ /dev/null @@ -1,32 +0,0 @@ -/* Copyright (C) 2015-2017 Carsten Pfeiffer - - This file is part of Gadgetbridge. - - Gadgetbridge is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Gadgetbridge is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . */ -package nodomain.freeyourgadget.gadgetbridge.database; - -/** - * Contains the configuration used for particular activity samples. - */ -public class UsedConfiguration { - String fwVersion; - String userName; - short userWeight; - short userSize; - // ... - int usedFrom; // timestamp - int usedUntil; // timestamp - short sleepGoal; // minutes - short stepsGoal; -} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java index 7c40f7304..46c85c820 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java @@ -124,4 +124,9 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { public int getBondingStyle(GBDevice device) { return BONDING_STYLE_ASK; } + + @Override + public boolean supportsActivityTracks() { + return false; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java index 91ed1df3b..f64e9f980 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java @@ -139,6 +139,14 @@ public interface DeviceCoordinator { */ boolean supportsActivityTracking(); + /** + * Indicates whether the device supports recording dedicated activity tracks, like + * walking, hiking, running, swimming, etc. and retrieving the recorded + * data. This is different from the constant activity tracking since the tracks are + * usually recorded with additional features, like e.g. GPS. + */ + boolean supportsActivityTracks(); + /** * Returns true if activity data fetching is supported AND possible at this * very moment. This will consider the device state (being connected/disconnected/busy...) @@ -228,5 +236,4 @@ public interface DeviceCoordinator { * This can be live HR, steps etc. */ boolean supportsRealtimeData(); - } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/amazfitbip/BipActivitySummary.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/amazfitbip/BipActivitySummary.java new file mode 100644 index 000000000..baf58485a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/amazfitbip/BipActivitySummary.java @@ -0,0 +1,206 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip; + +import java.util.Date; + +import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; + +public class BipActivitySummary extends BaseActivitySummary { + private int version; + private float distanceMeters; + private float ascentMeters; + private float descentMeters; + private float minAltitude; + private float maxAltitude; + private int minLatitude; + private int maxLatitude; + private int minLongitude; + private int maxLongitude; + private long steps; + private long activeTimeSeconds; + private float caloriesBurnt; + private float maxSpeed; + private float minPace; + private float maxPace; + private float totalStride; + private long timeAscent; + private long timeDescent; + private long timeFlat; + private int averageHR; + private int averagePace; + private int averageStride; + +// @Override +// public long getSteps() { +// return steps; +// } +// +// @Override +// public float getDistanceMeters() { +// return distanceMeters; +// } +// +// @Override +// public float getAscentMeters() { +// return ascentMeters; +// } +// +// @Override +// public float getDescentMeters() { +// return descentMeters; +// } +// +// @Override +// public float getMinAltitude() { +// return minAltitude; +// } +// +// @Override +// public float getMaxAltitude() { +// return maxAltitude; +// } +// +// @Override +// public float getCalories() { +// return caloriesBurnt; +// } +// +// @Override +// public float getMaxSpeed() { +// return maxSpeed; +// } +// +// @Override +// public float getMinSpeed() { +// return minPace; +// } +// +// @Override +// public float getAverageSpeed() { +// return averagePace; +// } + + public void setVersion(int version) { + this.version = version; + } + + public int getVersion() { + return version; + } + + public void setDistanceMeters(float distanceMeters) { + this.distanceMeters = distanceMeters; + } + + public void setAscentMeters(float ascentMeters) { + this.ascentMeters = ascentMeters; + } + + public void setDescentMeters(float descentMeters) { + this.descentMeters = descentMeters; + } + + public void setMinAltitude(float minAltitude) { + this.minAltitude = minAltitude; + } + + public void setMaxAltitude(float maxAltitude) { + this.maxAltitude = maxAltitude; + } + + public void setMinLatitude(int minLatitude) { + this.minLatitude = minLatitude; + } + + public void setMaxLatitude(int maxLatitude) { + this.maxLatitude = maxLatitude; + } + + public void setMinLongitude(int minLongitude) { + this.minLongitude = minLongitude; + } + + public void setMaxLongitude(int maxLongitude) { + this.maxLongitude = maxLongitude; + } + + public void setSteps(long steps) { + this.steps = steps; + } + + public void setActiveTimeSeconds(long activeTimeSeconds) { + this.activeTimeSeconds = activeTimeSeconds; + } + + public void setCaloriesBurnt(float caloriesBurnt) { + this.caloriesBurnt = caloriesBurnt; + } + + public void setMaxSpeed(float maxSpeed) { + this.maxSpeed = maxSpeed; + } + + public void setMinPace(float minPace) { + this.minPace = minPace; + } + + public void setMaxPace(float maxPace) { + this.maxPace = maxPace; + } + + public void setTotalStride(float totalStride) { + this.totalStride = totalStride; + } + + public float getTotalStride() { + return totalStride; + } + + public void setTimeAscent(long timeAscent) { + this.timeAscent = timeAscent; + } + + public long getTimeAscent() { + return timeAscent; + } + + public void setTimeDescent(long timeDescent) { + this.timeDescent = timeDescent; + } + + public long getTimeDescent() { + return timeDescent; + } + + public void setTimeFlat(long timeFlat) { + this.timeFlat = timeFlat; + } + + public long getTimeFlat() { + return timeFlat; + } + + public void setAverageHR(int averageHR) { + this.averageHR = averageHR; + } + + public int getAverageHR() { + return averageHR; + } + + public void setAveragePace(int averagePace) { + this.averagePace = averagePace; + } + + public int getAveragePace() { + return averagePace; + } + + public void setAverageStride(int averageStride) { + this.averageStride = averageStride; + } + + public int getAverageStride() { + return averageStride; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitbip/AmazfitBipCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitbip/AmazfitBipCoordinator.java index 812f34cdb..9c33c981b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitbip/AmazfitBipCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitbip/AmazfitBipCoordinator.java @@ -63,4 +63,9 @@ public class AmazfitBipCoordinator extends HuamiCoordinator { public boolean supportsHeartRateMeasurement(GBDevice device) { return true; } + + @Override + public boolean supportsActivityTracks() { + return true; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitbip/AmazfitBipService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitbip/AmazfitBipService.java index 130f5d5eb..649907c42 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitbip/AmazfitBipService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitbip/AmazfitBipService.java @@ -32,4 +32,7 @@ public class AmazfitBipService { public static final byte[] COMMAND_SET_LANGUAGE_SIMPLIFIED_CHINESE = new byte[]{ENDPOINT_DISPLAY, 0x13, 0x00, 0x00}; public static final byte[] COMMAND_SET_LANGUAGE_TRADITIONAL_CHINESE = new byte[]{ENDPOINT_DISPLAY, 0x13, 0x00, 0x01}; public static final byte[] COMMAND_SET_LANGUAGE_ENGLISH = new byte[]{ENDPOINT_DISPLAY, 0x13, 0x00, 0x02}; + + public static final byte COMMAND_ACTIVITY_DATA_TYPE_SPORTS_SUMMARIES = 0x05; + public static final byte COMMAND_ACTIVITY_DATA_TYPE_SPORTS_DETAILS = 0x06; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2SampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2SampleProvider.java index be6c7cd15..398e05b4a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2SampleProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2SampleProvider.java @@ -20,43 +20,14 @@ package nodomain.freeyourgadget.gadgetbridge.devices.miband; import java.util.List; import de.greenrobot.dao.query.QueryBuilder; +import nodomain.freeyourgadget.gadgetbridge.devices.miband2.MiBand2Const; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample; import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySampleDao; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; -import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband2.MiBand2Const.*; public class MiBand2SampleProvider extends AbstractMiBandSampleProvider { -// these come from Mi1 -// public static final int TYPE_LIGHT_SLEEP = 5; -// public static final int TYPE_ACTIVITY = -1; -// public static final int TYPE_UNKNOWN = -1; -// public static final int TYPE_NONWEAR = 3; -// public static final int TYPE_CHARGING = 6; - - - // observed the following values so far: - // 00 01 02 09 0a 0b 0c 10 11 - - // 0 = same activity kind as before - // 1 = light activity walking? - // 3 = definitely non-wear - // 9 = probably light sleep, definitely some kind of sleep - // 10 = ignore, except for hr (if valid) - // 11 = probably deep sleep - // 12 = definitely wake up - // 17 = definitely not sleep related - - public static final int TYPE_UNSET = -1; - public static final int TYPE_NO_CHANGE = 0; - public static final int TYPE_ACTIVITY = 1; - public static final int TYPE_RUNNING = 2; - public static final int TYPE_NONWEAR = 3; - public static final int TYPE_CHARGING = 6; - public static final int TYPE_LIGHT_SLEEP = 9; - public static final int TYPE_IGNORE = 10; - public static final int TYPE_DEEP_SLEEP = 11; - public static final int TYPE_WAKE_UP = 12; public MiBand2SampleProvider(GBDevice device, DaoSession session) { super(device, session); @@ -117,39 +88,11 @@ public class MiBand2SampleProvider extends AbstractMiBandSampleProvider { @Override public int normalizeType(int rawType) { - switch (rawType) { - case TYPE_DEEP_SLEEP: - return ActivityKind.TYPE_DEEP_SLEEP; - case TYPE_LIGHT_SLEEP: - return ActivityKind.TYPE_LIGHT_SLEEP; - case TYPE_ACTIVITY: - case TYPE_RUNNING: - case TYPE_WAKE_UP: - return ActivityKind.TYPE_ACTIVITY; - case TYPE_NONWEAR: - return ActivityKind.TYPE_NOT_WORN; - case TYPE_CHARGING: - return ActivityKind.TYPE_NOT_WORN; //I believe it's a safe assumption - default: - case TYPE_UNSET: // fall through - return ActivityKind.TYPE_UNKNOWN; - } + return MiBand2Const.toActivityKind(rawType); } @Override public int toRawActivityKind(int activityKind) { - switch (activityKind) { - case ActivityKind.TYPE_ACTIVITY: - return TYPE_ACTIVITY; - case ActivityKind.TYPE_DEEP_SLEEP: - return TYPE_DEEP_SLEEP; - case ActivityKind.TYPE_LIGHT_SLEEP: - return TYPE_LIGHT_SLEEP; - case ActivityKind.TYPE_NOT_WORN: - return TYPE_NONWEAR; - case ActivityKind.TYPE_UNKNOWN: // fall through - default: - return TYPE_UNSET; - } + return MiBand2Const.toRawActivityType(activityKind); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband2/MiBand2Const.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband2/MiBand2Const.java new file mode 100644 index 000000000..1e3585719 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband2/MiBand2Const.java @@ -0,0 +1,68 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.miband2; + +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; + +public class MiBand2Const { + // observed the following values so far: + // 00 01 02 09 0a 0b 0c 10 11 + + // 0 = same activity kind as before + // 1 = light activity walking? + // 3 = definitely non-wear + // 9 = probably light sleep, definitely some kind of sleep + // 10 = ignore, except for hr (if valid) + // 11 = probably deep sleep + // 12 = definitely wake up + // 17 = definitely not sleep related + + public static final int TYPE_UNSET = -1; + public static final int TYPE_NO_CHANGE = 0; + public static final int TYPE_ACTIVITY = 1; + public static final int TYPE_RUNNING = 2; + public static final int TYPE_NONWEAR = 3; + public static final int TYPE_RIDE_BIKE = 4; + public static final int TYPE_CHARGING = 6; + public static final int TYPE_LIGHT_SLEEP = 9; + public static final int TYPE_IGNORE = 10; + public static final int TYPE_DEEP_SLEEP = 11; + public static final int TYPE_WAKE_UP = 12; + + public static int toActivityKind(int rawType) { + switch (rawType) { + case TYPE_DEEP_SLEEP: + return ActivityKind.TYPE_DEEP_SLEEP; + case TYPE_LIGHT_SLEEP: + return ActivityKind.TYPE_LIGHT_SLEEP; + case TYPE_ACTIVITY: + case TYPE_RUNNING: + case TYPE_WAKE_UP: + return ActivityKind.TYPE_ACTIVITY; + case TYPE_NONWEAR: + return ActivityKind.TYPE_NOT_WORN; + case TYPE_CHARGING: + return ActivityKind.TYPE_NOT_WORN; //I believe it's a safe assumption + case TYPE_RIDE_BIKE: + return ActivityKind.TYPE_BIKING; + default: + case TYPE_UNSET: // fall through + return ActivityKind.TYPE_UNKNOWN; + } + } + + public static int toRawActivityType(int activityKind) { + switch (activityKind) { + case ActivityKind.TYPE_ACTIVITY: + return TYPE_ACTIVITY; + case ActivityKind.TYPE_DEEP_SLEEP: + return TYPE_DEEP_SLEEP; + case ActivityKind.TYPE_LIGHT_SLEEP: + return TYPE_LIGHT_SLEEP; + case ActivityKind.TYPE_NOT_WORN: + return TYPE_NONWEAR; + case ActivityKind.TYPE_UNKNOWN: // fall through + default: + return TYPE_UNSET; + } + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/export/ActivityTrackExporter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/export/ActivityTrackExporter.java new file mode 100644 index 000000000..914fcc666 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/export/ActivityTrackExporter.java @@ -0,0 +1,15 @@ +package nodomain.freeyourgadget.gadgetbridge.export; + +import android.support.annotation.NonNull; + +import java.io.File; +import java.io.IOException; + +import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack; + +public interface ActivityTrackExporter { + @NonNull + String getDefaultFileName(@NonNull ActivityTrack track); + + void performExport(ActivityTrack track, File targetFile) throws IOException; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/export/GPXExporter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/export/GPXExporter.java new file mode 100644 index 000000000..5ae0ba746 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/export/GPXExporter.java @@ -0,0 +1,158 @@ +package nodomain.freeyourgadget.gadgetbridge.export; + +import android.support.annotation.NonNull; +import android.util.Xml; + +import org.xmlpull.v1.XmlSerializer; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack; +import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; +import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; + +public class GPXExporter implements ActivityTrackExporter { + private static final String NS_DEFAULT = ""; + private static final String NS_DEFAULT_URI = "http://www.topografix.com/GPX/1/1"; + private static final String NS_DEFAULT_PREFIX = ""; + private static final String NS_TRACKPOINT_EXTENSION = "gpxtpx"; + private static final String NS_TRACKPOINT_EXTENSION_URI = "http://www.garmin.com/xmlschemas/TrackPointExtension/v1"; + private static final String NS_XSI_URI = "http://www.w3.org/2001/XMLSchema-instance"; + + private String creator; + private boolean includeHeartRate = true; + + @NonNull + @Override + public String getDefaultFileName(@NonNull ActivityTrack track) { + return FileUtils.makeValidFileName(track.getName()); + } + + @Override + public void performExport(ActivityTrack track, File targetFile) throws IOException { + String encoding = StandardCharsets.UTF_8.name(); + XmlSerializer ser = Xml.newSerializer(); + try { + ser.setOutput(new FileOutputStream(targetFile), encoding); + ser.startDocument(encoding, Boolean.TRUE); + ser.setPrefix("xsi", NS_XSI_URI); + ser.setPrefix(NS_DEFAULT_PREFIX, NS_DEFAULT); + + ser.startTag(NS_DEFAULT, "gpx"); + ser.attribute(NS_DEFAULT, "version", "1.1"); + ser.attribute(NS_DEFAULT, "creator", getCreator()); + ser.attribute(NS_XSI_URI, "schemaLocation", NS_DEFAULT_URI + " " + "http://www.topografix.com/GPX/1/1/gpx.xsd"); + + exportMetadata(ser, track); + exportTrack(ser, track); + + ser.endTag(NS_DEFAULT, "gpx"); + ser.endDocument(); + } finally { + ser.flush(); + } + } + + private void exportMetadata(XmlSerializer ser, ActivityTrack track) throws IOException { + ser.startTag(NS_DEFAULT, "metadata"); + ser.startTag(NS_DEFAULT, "name").text(track.getName()).endTag(NS_DEFAULT, "name"); + + ser.startTag(NS_DEFAULT, "author"); + ser.startTag(NS_DEFAULT, "name").text(track.getUser().getName()).endTag(NS_DEFAULT, "name"); + ser.endTag(NS_DEFAULT, "author"); + + ser.startTag(NS_DEFAULT, "time").text(formatTime(new Date())).endTag(NS_DEFAULT, "time"); + + ser.endTag(NS_DEFAULT, "metadata"); + } + + private String formatTime(Date date) { + return DateTimeUtils.formatIso8601(date); + } + + private void exportTrack(XmlSerializer ser, ActivityTrack track) throws IOException { + ser.startTag(NS_DEFAULT, "trk"); + ser.startTag(NS_DEFAULT, "trkseg"); + + List trackPoints = track.getTrackPoints(); + String source = getSource(track); + for (ActivityPoint point : trackPoints) { + exportTrackPoint(ser, point, source); + } + + ser.endTag(NS_DEFAULT, "trkseg"); + ser.endTag(NS_DEFAULT, "trk"); + } + + private String getSource(ActivityTrack track) { + return track.getDevice().getName(); + } + + private void exportTrackPoint(XmlSerializer ser, ActivityPoint point, String source) throws IOException { + GPSCoordinate location = point.getLocation(); + if (location == null) { + return; // skip invalid points, that just contain hr data, for example + } + ser.startTag(NS_DEFAULT, "trkpt"); + ser.attribute(NS_DEFAULT, "lon", formatLocation(location.getLongitude())); + ser.attribute(NS_DEFAULT, "lat", formatLocation(location.getLatitude())); + ser.startTag(NS_DEFAULT, "ele").text(formatLocation(location.getAltitude())).endTag(NS_DEFAULT, "ele"); + ser.startTag(NS_DEFAULT, "time").text(formatTime(point.getTime())).endTag(NS_DEFAULT, "time"); + String description = point.getDescription(); + if (description != null) { + ser.startTag(NS_DEFAULT, "desc").text(description).endTag(NS_DEFAULT, "desc"); + } + ser.startTag(NS_DEFAULT, "src").text(source).endTag(NS_DEFAULT, "src"); + + exportTrackpointExtensions(ser, point); + + ser.endTag(NS_DEFAULT, "trkpt"); + } + + private void exportTrackpointExtensions(XmlSerializer ser, ActivityPoint point) throws IOException { + if (!includeHeartRate) { + return; + } + + int hr = point.getHeartRate(); + if (!HeartRateUtils.isValidHeartRateValue(hr)) { + return; + } + ser.startTag(NS_DEFAULT, "extensions"); + + ser.setPrefix(NS_TRACKPOINT_EXTENSION, NS_TRACKPOINT_EXTENSION_URI); + ser.startTag(NS_TRACKPOINT_EXTENSION_URI, "hr").text(String.valueOf(hr)).endTag(NS_TRACKPOINT_EXTENSION_URI, "hr"); + + ser.endTag(NS_DEFAULT, "extensions"); + } + + private String formatLocation(double value) { + return new BigDecimal(value).setScale(GPSCoordinate.GPS_DECIMAL_DEGREES_SCALE, RoundingMode.HALF_UP).toPlainString(); + } + + public String getCreator() { + return creator; // TODO: move to some kind of BrandingInfo class + } + + public void setCreator(String creator) { + this.creator = creator; + } + + public void setIncludeHeartRate(boolean includeHeartRate) { + this.includeHeartRate = includeHeartRate; + } + + public boolean isIncludeHeartRate() { + return includeHeartRate; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityKind.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityKind.java index 744a39f50..b8108bebf 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityKind.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityKind.java @@ -17,8 +17,12 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.model; +import android.content.Context; +import android.support.annotation.DrawableRes; + import java.util.Arrays; +import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; public class ActivityKind { @@ -28,9 +32,13 @@ public class ActivityKind { public static final int TYPE_LIGHT_SLEEP = 2; public static final int TYPE_DEEP_SLEEP = 4; public static final int TYPE_NOT_WORN = 8; + public static final int TYPE_RUNNING = 16; + public static final int TYPE_WALKING = 32; + public static final int TYPE_SWIMMING = 64; public static final int TYPE_SLEEP = TYPE_LIGHT_SLEEP | TYPE_DEEP_SLEEP; public static final int TYPE_ALL = TYPE_ACTIVITY | TYPE_SLEEP | TYPE_NOT_WORN; + public static final int TYPE_BIKING = 128; public static int[] mapToDBActivityTypes(int types, SampleProvider provider) { int[] result = new int[3]; @@ -47,7 +55,68 @@ public class ActivityKind { if ((types & ActivityKind.TYPE_NOT_WORN) != 0) { result[i++] = provider.toRawActivityKind(TYPE_NOT_WORN); } + if ((types & ActivityKind.TYPE_RUNNING) != 0) { + result[i++] = provider.toRawActivityKind(TYPE_RUNNING); + } + if ((types & ActivityKind.TYPE_WALKING) != 0) { + result[i++] = provider.toRawActivityKind(TYPE_WALKING); + } + if ((types & ActivityKind.TYPE_SWIMMING) != 0) { + result[i++] = provider.toRawActivityKind(TYPE_SWIMMING); + } + if ((types & ActivityKind.TYPE_BIKING) != 0) { + result[i++] = provider.toRawActivityKind(TYPE_BIKING); + } return Arrays.copyOf(result, i); } + public static String asString(int kind, Context context) { + switch (kind) { + case TYPE_NOT_MEASURED: + return context.getString(R.string.activity_type_not_measured); + case TYPE_ACTIVITY: + return context.getString(R.string.activity_type_activity); + case TYPE_LIGHT_SLEEP: + return context.getString(R.string.activity_type_light_sleep); + case TYPE_DEEP_SLEEP: + return context.getString(R.string.activity_type_deep_sleep); + case TYPE_NOT_WORN: + return context.getString(R.string.activity_type_not_worn); + case TYPE_RUNNING: + return context.getString(R.string.activity_type_running); + case TYPE_WALKING: + return context.getString(R.string.activity_type_walking); + case TYPE_SWIMMING: + return context.getString(R.string.activity_type_swimming); + case TYPE_BIKING: + return context.getString(R.string.activity_type_biking); + case TYPE_UNKNOWN: + default: + return context.getString(R.string.activity_type_unknown); + } + } + + @DrawableRes + public static int getIconId(int kind) { + switch (kind) { + case TYPE_NOT_MEASURED: + return R.drawable.ic_activity_not_measured; + case TYPE_LIGHT_SLEEP: + return R.drawable.ic_activity_light_sleep; + case TYPE_DEEP_SLEEP: + return R.drawable.ic_activity_deep_sleep; + case TYPE_RUNNING: + return R.drawable.ic_activity_running; + case TYPE_WALKING: + return R.drawable.ic_activity_walking; + case TYPE_BIKING: + return R.drawable.ic_activity_biking; + case TYPE_SWIMMING: // fall through + case TYPE_NOT_WORN: // fall through + case TYPE_ACTIVITY: // fall through + case TYPE_UNKNOWN: // fall through + default: + return R.drawable.ic_activity_unknown; + } + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityPoint.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityPoint.java new file mode 100644 index 000000000..da0c6d2c4 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityPoint.java @@ -0,0 +1,95 @@ +package nodomain.freeyourgadget.gadgetbridge.model; + +import android.support.annotation.Nullable; + +import java.util.Date; + +// https://www8.garmin.com/xmlschemas/TrackPointExtensionv1.xsd +/* + + 29.2 + + + + 11 + 92 + 0 + + + +*/ +public class ActivityPoint { + private Date time; + private GPSCoordinate location; + private int heartRate; + private long speed4; + private long speed5; + private long speed6; + + // e.g. to describe a pause during the activity + private @Nullable String description; + + public ActivityPoint() { + } + + public ActivityPoint(Date time) { + this.time = time; + } + + public Date getTime() { + return time; + } + + public void setTime(Date time) { + this.time = time; + } + + @Nullable + public String getDescription() { + return description; + } + + public void setDescription(@Nullable String description) { + this.description = description; + } + + public GPSCoordinate getLocation() { + return location; + } + + public void setLocation(GPSCoordinate location) { + this.location = location; + } + + public int getHeartRate() { + return heartRate; + } + + public void setHeartRate(int heartRate) { + this.heartRate = heartRate; + } + + public long getSpeed4() { + return speed4; + } + + public void setSpeed4(long speed4) { + this.speed4 = speed4; + } + + public long getSpeed5() { + return speed5; + } + + public void setSpeed5(long speed5) { + this.speed5 = speed5; + } + + public long getSpeed6() { + return speed6; + } + + public void setSpeed6(long speed6) { + this.speed6 = speed6; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummary.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummary.java new file mode 100644 index 000000000..9bb670a1f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummary.java @@ -0,0 +1,33 @@ +package nodomain.freeyourgadget.gadgetbridge.model; + +import java.io.Serializable; +import java.util.Date; + +/** + * Summarized information about a temporal activity. + * + * // TODO: split into separate entities? + */ +public interface ActivitySummary extends Serializable { + String getName(); + Date getStartTime(); + Date getEndTime(); + + int getActivityKind(); + String getGpxTrack(); + + long getDeviceId(); + + long getUserId(); + // long getSteps(); +// float getDistanceMeters(); +// float getAscentMeters(); +// float getDescentMeters(); +// float getMinAltitude(); +// float getMaxAltitude(); +// float getCalories(); +// +// float getMaxSpeed(); +// float getMinSpeed(); +// float getAverageSpeed(); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityTrack.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityTrack.java new file mode 100644 index 000000000..22507c312 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityTrack.java @@ -0,0 +1,62 @@ +package nodomain.freeyourgadget.gadgetbridge.model; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.User; + +public class ActivityTrack { + private Date baseTime; + private Device device; + private User user; + private String name; + + + public void setBaseTime(Date baseTime) { + this.baseTime = baseTime; + } + + public Device getDevice() { + return device; + } + + public void setDevice(Device device) { + this.device = device; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public void setTrackPoints(List trackPoints) { + this.trackPoints = trackPoints; + } + + private List trackPoints = new ArrayList<>(); + + public void addTrackPoint(ActivityPoint point) { + trackPoints.add(point); + } + + public List getTrackPoints() { + return trackPoints; + } + + public Date getBaseTime() { + return baseTime; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/GPSCoordinate.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/GPSCoordinate.java new file mode 100644 index 000000000..e8b51d76c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/GPSCoordinate.java @@ -0,0 +1,65 @@ +package nodomain.freeyourgadget.gadgetbridge.model; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +public final class GPSCoordinate { + private final double latitude; + private final double longitude; + private final double altitude; + + public static final int GPS_DECIMAL_DEGREES_SCALE = 6; // precise to 111.132mm at equator: https://en.wikipedia.org/wiki/Decimal_degrees + + public GPSCoordinate(double longitude, double latitude, double altitude) { + this.longitude = longitude; + this.latitude = latitude; + this.altitude = altitude; + } + + public double getLatitude() { + return latitude; + } + + public double getLongitude() { + return longitude; + } + + public double getAltitude() { + return altitude; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + GPSCoordinate that = (GPSCoordinate) o; + + if (Double.compare(that.getLatitude(), getLatitude()) != 0) return false; + if (Double.compare(that.getLongitude(), getLongitude()) != 0) return false; + return Double.compare(that.getAltitude(), getAltitude()) == 0; + + } + + @Override + public int hashCode() { + int result; + long temp; + temp = Double.doubleToLongBits(getLatitude()); + result = (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(getLongitude()); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(getAltitude()); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + private String formatLocation(double value) { + return new BigDecimal(value).setScale(8, RoundingMode.HALF_UP).toPlainString(); + } + + @Override + public String toString() { + return "lon: " + formatLocation(longitude) + ", lat: " + formatLocation(latitude) + ", alt: " + formatLocation(altitude) + "m"; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java index 7411691f0..d09e8ad51 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java @@ -29,6 +29,9 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotificat * Provides methods to convert standard BLE units to byte sequences and vice versa. */ public class BLETypeConversions { + public static final int TZ_FLAG_NONE = 0; + public static final int TZ_FLAG_INCLUDE_DST_IN_TZ = 1; + /** * Converts a timestamp to the byte sequence to be sent to the current time characteristic * @@ -157,10 +160,33 @@ public class BLETypeConversions { return createCalendar(); } + public static long toUnsigned(int unsignedInt) { + return ((long) unsignedInt) & 0xffffffffL; + } + public static int toUnsigned(short value) { + return value & 0xffff; + } + + public static int toUnsigned(byte value) { + return value & 0xff; + } + + public static int toUint16(byte value) { + return toUnsigned(value); + } + public static int toUint16(byte... bytes) { return (bytes[0] & 0xff) | ((bytes[1] & 0xff) << 8); } + public static int toInt16(byte... bytes) { + return (short) (bytes[0] & 0xff | ((bytes[1] & 0xff) << 8)); + } + + public static int toUint32(byte... bytes) { + return (bytes[0] & 0xff) | ((bytes[1] & 0xff) << 8) | ((bytes[2] & 0xff) << 16) | ((bytes[3] & 0xff) << 24); + } + public static byte[] fromUint16(int value) { return new byte[] { (byte) (value & 0xff), @@ -223,7 +249,20 @@ public class BLETypeConversions { * @return sint8 value from -48..+56 */ public static byte mapTimeZone(TimeZone timeZone) { - int utcOffsetInHours = (timeZone.getRawOffset() / (1000 * 60 * 60)); + return mapTimeZone(timeZone, TZ_FLAG_NONE); + } + + /** + * https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.time_zone.xml + * @param timeZone + * @return sint8 value from -48..+56 + */ + public static byte mapTimeZone(TimeZone timeZone, int timezoneFlags) { + int offsetMillis = timeZone.getRawOffset(); + if (timezoneFlags == TZ_FLAG_INCLUDE_DST_IN_TZ) { + offsetMillis += timeZone.getDSTSavings(); + } + int utcOffsetInHours = (offsetMillis / (1000 * 60 * 60)); return (byte) (utcOffsetInHours * 4); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java index 1feff2946..6d864c504 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java @@ -38,6 +38,7 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; +import nodomain.freeyourgadget.gadgetbridge.Logging; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.State; import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport; @@ -467,10 +468,7 @@ public final class BtLEQueue { public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { if (LOG.isDebugEnabled()) { - String content = ""; - for (byte b : characteristic.getValue()) { - content += String.format(" 0x%1x", b); - } + String content = Logging.formatBytes(characteristic.getValue()); LOG.debug("characteristic changed: " + characteristic.getUuid() + " value: " + content); } if (!checkCorrectGattInstance(gatt, "characteristic changed")) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/ActivityDetailsParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/ActivityDetailsParser.java new file mode 100644 index 000000000..ae1ef5d13 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/ActivityDetailsParser.java @@ -0,0 +1,216 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.amazfitbip; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Date; + +import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack; +import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate; +import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; + +public class ActivityDetailsParser { + private static final Logger LOG = LoggerFactory.getLogger(ActivityDetailsParser.class); + + private static final byte TYPE_GPS = 0; + private static final byte TYPE_HR = 1; + private static final byte TYPE_UNKNOWN2 = 2; + private static final byte TYPE_PAUSE = 3; + private static final byte TYPE_SPEED4 = 4; + private static final byte TYPE_SPEED5 = 5; + private static final byte TYPE_GPS_SPEED6 = 6; + + public static final BigDecimal HUAMI_TO_DECIMAL_DEGREES_DIVISOR = new BigDecimal(3000000.0); + private final BaseActivitySummary summary; + private final ActivityTrack activityTrack; +// private final int version; + private final Date baseDate; + private long baseLongitude; + private long baseLatitude; + private int baseAltitude; + private ActivityPoint lastActivityPoint; + + public boolean getSkipCounterByte() { + return skipCounterByte; + } + + public void setSkipCounterByte(boolean skipCounterByte) { + this.skipCounterByte = skipCounterByte; + } + + private boolean skipCounterByte; + + public ActivityDetailsParser(BaseActivitySummary summary) { + this.summary = summary; +// this.version = version; +// this.baseDate = baseDate; +// + this.baseLongitude = summary.getBaseLongitude(); + this.baseLatitude = summary.getBaseLatitude(); + this.baseAltitude = summary.getBaseAltitude(); + this.baseDate = summary.getStartTime(); + + this.activityTrack = new ActivityTrack(); + activityTrack.setUser(summary.getUser()); + activityTrack.setDevice(summary.getDevice()); + activityTrack.setName(summary.getName() + "-" + summary.getId()); + } + + public ActivityTrack parse(byte[] bytes) throws GBException { + int i = 0; + try { + long totalTimeOffset = 0; + int lastTimeOffset = 0; + while (i < bytes.length) { + if (skipCounterByte && (i % 17) == 0) { + i++; + } + + byte type = bytes[i++]; + int timeOffset = BLETypeConversions.toUnsigned(bytes[i++]); + // handle timeOffset overflows (1 byte, always increasing, relative to base) + if (lastTimeOffset <= timeOffset) { + timeOffset = timeOffset - lastTimeOffset; + lastTimeOffset += timeOffset; + } else { + lastTimeOffset = timeOffset; + } + totalTimeOffset += timeOffset; + + switch (type) { + case TYPE_GPS: + i += consumeGPSAndUpdateBaseLocation(bytes, i, totalTimeOffset); + break; + case TYPE_HR: + i += consumeHeartRate(bytes, i, totalTimeOffset); + break; + case TYPE_UNKNOWN2: + i += consumeUnknown2(bytes, i); + break; + case TYPE_PAUSE: + i += consumePause(bytes, i); + break; + case TYPE_SPEED4: + i += consumeSpeed4(bytes, i); + break; + case TYPE_SPEED5: + i += consumeSpeed5(bytes, i); + break; + case TYPE_GPS_SPEED6: + i += consumeSpeed6(bytes, i); + break; + } + } + } catch (IndexOutOfBoundsException ex) { + throw new GBException("Error parsing activity details: " + ex.getMessage(), ex); + } + + return activityTrack; + } + + private int consumeGPSAndUpdateBaseLocation(byte[] bytes, int offset, long timeOffset) { + int i = 0; + int longitudeDelta = BLETypeConversions.toInt16(bytes[offset + i++], bytes[offset + i++]); + int latitudeDelta = BLETypeConversions.toInt16(bytes[offset + i++], bytes[offset + i++]); + int altitudeDelta = BLETypeConversions.toInt16(bytes[offset + i++], bytes[offset + i++]); + + baseLongitude += longitudeDelta; + baseLatitude += latitudeDelta; + baseAltitude += altitudeDelta; + + GPSCoordinate coordinate = new GPSCoordinate( + convertHuamiValueToDecimalDegrees(baseLongitude), + convertHuamiValueToDecimalDegrees(baseLatitude), + baseAltitude); + + ActivityPoint ap = getActivityPointFor(timeOffset); + ap.setLocation(coordinate); + add(ap); + + return i; + } + + private double convertHuamiValueToDecimalDegrees(long huamiValue) { + BigDecimal result = new BigDecimal(huamiValue).divide(HUAMI_TO_DECIMAL_DEGREES_DIVISOR, GPSCoordinate.GPS_DECIMAL_DEGREES_SCALE, RoundingMode.HALF_UP); + return result.doubleValue(); + } + + private int consumeHeartRate(byte[] bytes, int offset, long timeOffsetSeconds) { + int v1 = BLETypeConversions.toUint16(bytes[offset]); + int v2 = BLETypeConversions.toUint16(bytes[offset + 1]); + int v3 = BLETypeConversions.toUint16(bytes[offset + 2]); + int v4 = BLETypeConversions.toUint16(bytes[offset + 3]); + int v5 = BLETypeConversions.toUint16(bytes[offset + 4]); + int v6 = BLETypeConversions.toUint16(bytes[offset + 5]); + + if (v2 == 0 && v3 == 0 && v4 == 0 && v5 == 0 && v6 == 0) { + // new version +// LOG.info("detected heart rate in 'new' version, where version is: " + summary.getVersion()); + LOG.info("detected heart rate in 'new' version format"); + ActivityPoint ap = getActivityPointFor(timeOffsetSeconds); + ap.setHeartRate(v1); + add(ap); + } else { + ActivityPoint ap = getActivityPointFor(v1); + ap.setHeartRate(v2); + add(ap); + + ap = getActivityPointFor(v3); + ap.setHeartRate(v4); + add(ap); + + ap = getActivityPointFor(v5); + ap.setHeartRate(v6); + add(ap); + } + return 6; + } + + private ActivityPoint getActivityPointFor(long timeOffsetSeconds) { + Date time = makeAbsolute(timeOffsetSeconds); +// if (lastActivityPoint != null) { +// if (lastActivityPoint.getTime().equals(time)) { +// return lastActivityPoint; +// } +// } + return new ActivityPoint(time); + } + + private Date makeAbsolute(long timeOffsetSeconds) { + return new Date(baseDate.getTime() + timeOffsetSeconds * 1000); + } + + private void add(ActivityPoint ap) { + if (ap != lastActivityPoint) { + lastActivityPoint = ap; + activityTrack.addTrackPoint(ap); + } else { + LOG.info("skipping point!"); + } + } + + private int consumeUnknown2(byte[] bytes, int offset) { + return 6; // just guessing... + } + + private int consumePause(byte[] bytes, int i) { + return 6; // just guessing... + } + + private int consumeSpeed4(byte[] bytes, int offset) { + return 6; + } + + private int consumeSpeed5(byte[] bytes, int offset) { + return 6; + } + + private int consumeSpeed6(byte[] bytes, int offset) { + return 6; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/AmazfitBipSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/AmazfitBipSupport.java index f590c6a4c..70281c47b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/AmazfitBipSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/AmazfitBipSupport.java @@ -44,10 +44,10 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.AlertCategory; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.AlertNotificationProfile; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.NewAlert; -import nodomain.freeyourgadget.gadgetbridge.service.devices.amazfitbip.operations.AmazfitBipFetchLogsOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiIcon; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.NotificationStrategy; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.MiBand2Support; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.FetchSportsSummaryOperation; import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; import nodomain.freeyourgadget.gadgetbridge.util.Version; @@ -183,7 +183,8 @@ public class AmazfitBipSupport extends MiBand2Support { @Override public void onTestNewFunction() { try { - new AmazfitBipFetchLogsOperation(this).perform(); +// new AmazfitBipFetchLogsOperation(this).perform(); + new FetchSportsSummaryOperation(this).perform(); } catch (IOException ex) { LOG.error("Unable to fetch logs", ex); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/operations/AmazfitBipFetchLogsOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/operations/AmazfitBipFetchLogsOperation.java index c1026a8f1..dd47022d4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/operations/AmazfitBipFetchLogsOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/operations/AmazfitBipFetchLogsOperation.java @@ -89,7 +89,7 @@ public class AmazfitBipFetchLogsOperation extends AbstractFetchOperation { } @Override - protected void handleActivityFetchFinish() { + protected void handleActivityFetchFinish(boolean success) { LOG.info("Fetching log data has finished"); try { logOutputStream.close(); @@ -98,7 +98,7 @@ public class AmazfitBipFetchLogsOperation extends AbstractFetchOperation { LOG.warn("could not close output stream", e); return; } - super.handleActivityFetchFinish(); + super.handleActivityFetchFinish(success); } @Override @@ -114,17 +114,17 @@ public class AmazfitBipFetchLogsOperation extends AbstractFetchOperation { bufferActivityData(value); } else { GB.toast("Error fetching activity data, invalid package counter: " + value[0], Toast.LENGTH_LONG, GB.ERROR); - handleActivityFetchFinish(); + handleActivityFetchFinish(false); } } @Override protected void bufferActivityData(@NonNull byte[] value) { try { - logOutputStream.write(Arrays.copyOfRange(value, 1, value.length)); + logOutputStream.write(value, 1, value.length); } catch (IOException e) { LOG.warn("could not write to output stream", e); - handleActivityFetchFinish(); + handleActivityFetchFinish(false); } } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/MiBand2Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/MiBand2Support.java index a83ba843f..57e4fdc4b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/MiBand2Support.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/MiBand2Support.java @@ -40,6 +40,7 @@ import java.util.Date; import java.util.GregorianCalendar; import java.util.List; import java.util.Set; +import java.util.TimeZone; import java.util.Timer; import java.util.TimerTask; import java.util.UUID; @@ -66,6 +67,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandService; import nodomain.freeyourgadget.gadgetbridge.devices.miband.VibrationProfile; +import nodomain.freeyourgadget.gadgetbridge.devices.miband2.MiBand2Const; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample; @@ -102,6 +104,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.NotificationS import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.RealtimeSamplesSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.actions.StopNotificationAction; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.FetchActivityOperation; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.FetchSportsSummaryOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.InitOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.UpdateFirmwareOperation; import nodomain.freeyourgadget.gadgetbridge.util.GB; @@ -209,6 +212,14 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { return builder; } + /** + * Returns the given date/time (calendar) as a byte sequence, suitable for sending to the + * Mi Band 2 (or derivative). The band appears to not handle DST offsets, so we simply add this + * to the timezone. + * @param calendar + * @param precision + * @return + */ public byte[] getTimeBytes(Calendar calendar, TimeUnit precision) { byte[] bytes; if (precision == TimeUnit.MINUTES) { @@ -218,7 +229,8 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { } else { throw new IllegalArgumentException("Unsupported precision, only MINUTES and SECONDS are supported till now"); } - byte[] tail = new byte[] { 0, BLETypeConversions.mapTimeZone(calendar.getTimeZone()) }; // 0 = adjust reason bitflags? or DST offset?? , timezone + byte[] tail = new byte[] { 0, BLETypeConversions.mapTimeZone(calendar.getTimeZone(), BLETypeConversions.TZ_FLAG_INCLUDE_DST_IN_TZ) }; + // 0 = adjust reason bitflags? or DST offset?? , timezone // byte[] tail = new byte[] { 0x2 }; // reason byte[] all = BLETypeConversions.join(bytes, tail); return all; @@ -1137,7 +1149,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { sample.setHeartRate(getHeartrateBpm()); sample.setSteps(getSteps()); sample.setRawIntensity(ActivitySample.NOT_MEASURED); - sample.setRawKind(MiBand2SampleProvider.TYPE_ACTIVITY); // to make it visible in the charts TODO: add a MANUAL kind for that? + sample.setRawKind(MiBand2Const.TYPE_ACTIVITY); // to make it visible in the charts TODO: add a MANUAL kind for that? provider.addGBActivitySample(sample); @@ -1318,10 +1330,9 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { @Override public void onTestNewFunction() { try { - TransactionBuilder builder = performInitialized("test realtime steps"); - builder.read(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_7_REALTIME_STEPS)); - builder.queue(getQueue()); - } catch (IOException e) { + new FetchSportsSummaryOperation(this).perform(); + } catch (IOException ex) { + LOG.error("Unable to fetch MI activity data", ex); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/AbstractFetchOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/AbstractFetchOperation.java index ffa73fab8..2e399e1e6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/AbstractFetchOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/AbstractFetchOperation.java @@ -115,7 +115,7 @@ public abstract class AbstractFetchOperation extends AbstractMiBand2Operation { } @CallSuper - protected void handleActivityFetchFinish() { + protected void handleActivityFetchFinish(boolean success) { operationFinished(); unsetBusy(); } @@ -149,18 +149,18 @@ public abstract class AbstractFetchOperation extends AbstractMiBand2Operation { DateFormat.getDateTimeInstance().format(startTimestamp.getTime())), Toast.LENGTH_LONG, GB.INFO); } else { LOG.warn("Unexpected activity metadata: " + Logging.formatBytes(value)); - handleActivityFetchFinish(); + handleActivityFetchFinish(false); } } else if (value.length == 3) { if (Arrays.equals(MiBand2Service.RESPONSE_FINISH_SUCCESS, value)) { - handleActivityFetchFinish(); + handleActivityFetchFinish(true); } else { LOG.warn("Unexpected activity metadata: " + Logging.formatBytes(value)); - handleActivityFetchFinish(); + handleActivityFetchFinish(false); } } else { LOG.warn("Unexpected activity metadata: " + Logging.formatBytes(value)); - handleActivityFetchFinish(); + handleActivityFetchFinish(false); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchActivityOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchActivityOperation.java index d87d10235..8f9203012 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchActivityOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchActivityOperation.java @@ -74,7 +74,7 @@ public class FetchActivityOperation extends AbstractFetchOperation { builder.write(characteristicFetch, new byte[] { MiBand2Service.COMMAND_FETCH_DATA}); } - protected void handleActivityFetchFinish() { + protected void handleActivityFetchFinish(boolean success) { LOG.info("Fetching activity data has finished round " + fetchCount); GregorianCalendar lastSyncTimestamp = saveSamples(); if (lastSyncTimestamp != null && needsAnotherFetch(lastSyncTimestamp)) { @@ -86,7 +86,7 @@ public class FetchActivityOperation extends AbstractFetchOperation { } } - super.handleActivityFetchFinish(); + super.handleActivityFetchFinish(success); } private boolean needsAnotherFetch(GregorianCalendar lastSyncTimestamp) { @@ -167,11 +167,12 @@ public class FetchActivityOperation extends AbstractFetchOperation { bufferActivityData(value); } else { GB.toast("Error fetching activity data, invalid package counter: " + value[0], Toast.LENGTH_LONG, GB.ERROR); - handleActivityFetchFinish(); + handleActivityFetchFinish(false); return; } } else { GB.toast("Error fetching activity data, unexpected package length: " + value.length, Toast.LENGTH_LONG, GB.ERROR); + handleActivityFetchFinish(false); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsDetailsOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsDetailsOperation.java new file mode 100644 index 000000000..9ea748525 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsDetailsOperation.java @@ -0,0 +1,174 @@ +/* Copyright (C) 2017 Andreas Shimokawa, Carsten Pfeiffer + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations; + +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.util.GregorianCalendar; +import java.util.concurrent.TimeUnit; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.Logging; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipService; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service; +import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary; +import nodomain.freeyourgadget.gadgetbridge.export.ActivityTrackExporter; +import nodomain.freeyourgadget.gadgetbridge.export.GPXExporter; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack; +import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WaitAction; +import nodomain.freeyourgadget.gadgetbridge.service.devices.amazfitbip.ActivityDetailsParser; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.MiBand2Support; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; +import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +/** + * An operation that fetches activity data. For every fetch, a new operation must + * be created, i.e. an operation may not be reused for multiple fetches. + */ +public class FetchSportsDetailsOperation extends AbstractFetchOperation { + private static final Logger LOG = LoggerFactory.getLogger(FetchSportsDetailsOperation.class); + private final BaseActivitySummary summary; + + private ByteArrayOutputStream buffer; + + public FetchSportsDetailsOperation(BaseActivitySummary summary, MiBand2Support support) { + super(support); + this.summary = summary; + } + + @Override + protected void startFetching(TransactionBuilder builder) { + buffer = new ByteArrayOutputStream(1024); + GregorianCalendar sinceWhen = getLastSuccessfulSyncTime(); + builder.write(characteristicFetch, BLETypeConversions.join(new byte[] { + MiBand2Service.COMMAND_ACTIVITY_DATA_START_DATE, + AmazfitBipService.COMMAND_ACTIVITY_DATA_TYPE_SPORTS_DETAILS}, + getSupport().getTimeBytes(sinceWhen, TimeUnit.MINUTES))); + builder.add(new WaitAction(1000)); // TODO: actually wait for the success-reply + builder.notify(characteristicActivityData, true); + builder.write(characteristicFetch, new byte[] { MiBand2Service.COMMAND_FETCH_DATA }); + } + + @Override + protected void handleActivityFetchFinish(boolean success) { + LOG.info("Fetching activity data has finished round " + fetchCount); +// GregorianCalendar lastSyncTimestamp = saveSamples(); +// if (lastSyncTimestamp != null && needsAnotherFetch(lastSyncTimestamp)) { +// try { +// startFetching(); +// return; +// } catch (IOException ex) { +// LOG.error("Error starting another round of fetching activity data", ex); +// } +// } + + + if (success) { + ActivityDetailsParser parser = new ActivityDetailsParser(summary); + parser.setSkipCounterByte(false); // is already stripped + try { + ActivityTrack track = parser.parse(buffer.toByteArray()); + ActivityTrackExporter exporter = createExporter(); + String fileName = FileUtils.makeValidFileName("gadgetbridge-track-" + DateTimeUtils.formatIso8601(summary.getStartTime())); + File targetFile = new File(FileUtils.getExternalFilesDir(), fileName); + exporter.performExport(track, targetFile); + + try (DBHandler dbHandler = GBApplication.acquireDB()) { + summary.setGpxTrack(targetFile.getAbsolutePath()); + dbHandler.getDaoSession().getBaseActivitySummaryDao().update(summary); + } + } catch (Exception ex) { + GB.toast(getContext(), "Error getting activity details: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex); + } + } + + super.handleActivityFetchFinish(success); + } + + protected ActivityTrackExporter createExporter() { + GPXExporter exporter = new GPXExporter(); + exporter.setCreator(GBApplication.app().getNameAndVersion()); + return exporter; + } + + /** + * Method to handle the incoming activity data. + * There are two kind of messages we currently know: + * - the first one is 11 bytes long and contains metadata (how many bytes to expect, when the data starts, etc.) + * - the second one is 20 bytes long and contains the actual activity data + *

+ * The first message type is parsed by this method, for every other length of the value param, bufferActivityData is called. + * + * @param value + */ + @Override + protected void handleActivityNotif(byte[] value) { + LOG.warn("sports data: " + Logging.formatBytes(value)); + + if (!isOperationRunning()) { + LOG.error("ignoring activity data notification because operation is not running. Data length: " + value.length); + getSupport().logMessageContent(value); + return; + } + + if (value.length < 2) { + LOG.error("unexpected sports summary data length: " + value.length); + getSupport().logMessageContent(value); + return; + } + + if ((byte) (lastPacketCounter + 1) == value[0] ) { + lastPacketCounter++; + bufferActivityData(value); + } else { + GB.toast("Error fetching activity data, invalid package counter: " + value[0] + ", last was: " + lastPacketCounter, Toast.LENGTH_LONG, GB.ERROR); + handleActivityFetchFinish(false); + return; + } + } + + /** + * Buffers the given activity summary data. If the total size is reached, + * it is converted to an object and saved in the database. + * @param value + */ + @Override + protected void bufferActivityData(byte[] value) { + buffer.write(value, 1, value.length - 1); // skip the counter + } + + @Override + protected String getLastSyncTimeKey() { + return getDevice().getAddress() + "_" + "lastSportsSummaryTimeMillis"; + } + + protected GregorianCalendar getLastSuccessfulSyncTime() { + // FIXME: remove this! + GregorianCalendar calendar = BLETypeConversions.createCalendar(); + calendar.setTime(summary.getStartTime()); + return calendar; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsSummaryOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsSummaryOperation.java index 5ae6b2d83..ebb0d2534 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsSummaryOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsSummaryOperation.java @@ -23,12 +23,30 @@ import android.widget.Toast; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.util.Calendar; +import java.util.Date; import java.util.GregorianCalendar; +import java.util.concurrent.TimeUnit; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.Logging; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipService; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service; +import nodomain.freeyourgadget.gadgetbridge.devices.miband2.MiBand2Const; +import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.User; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WaitAction; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.MiBand2Support; import nodomain.freeyourgadget.gadgetbridge.util.GB; @@ -39,9 +57,9 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB; public class FetchSportsSummaryOperation extends AbstractFetchOperation { private static final Logger LOG = LoggerFactory.getLogger(FetchSportsSummaryOperation.class); -// private List samples = new ArrayList<>(60*24); // 1day per default + private ByteArrayOutputStream buffer = new ByteArrayOutputStream(140); - private byte lastPacketCounter; +// private List samples = new ArrayList<>(60*24); // 1day per default public FetchSportsSummaryOperation(MiBand2Support support) { super(support); @@ -50,18 +68,19 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation { @Override protected void startFetching(TransactionBuilder builder) { GregorianCalendar sinceWhen = getLastSuccessfulSyncTime(); -// builder.write(characteristicFetch, BLETypeConversions.join(new byte[] { -// MiBand2Service.COMMAND_ACTIVITY_DATA_START_DATE, -// AmazfitBipService.COMMAND_ACTIVITY_DATA_TYPE_SPORTS_SUMMARIES}, -// getSupport().getTimeBytes(sinceWhen, TimeUnit.MINUTES))); -// builder.add(new WaitAction(1000)); // TODO: actually wait for the success-reply -// builder.notify(characteristicActivityData, true); -// builder.write(characteristicFetch, new byte[] { MiBand2Service.COMMAND_FETCH_DATA }); + builder.write(characteristicFetch, BLETypeConversions.join(new byte[] { + MiBand2Service.COMMAND_ACTIVITY_DATA_START_DATE, + AmazfitBipService.COMMAND_ACTIVITY_DATA_TYPE_SPORTS_SUMMARIES}, + getSupport().getTimeBytes(sinceWhen, TimeUnit.MINUTES))); + builder.add(new WaitAction(1000)); // TODO: actually wait for the success-reply + builder.notify(characteristicActivityData, true); + builder.write(characteristicFetch, new byte[] { MiBand2Service.COMMAND_FETCH_DATA }); } @Override - protected void handleActivityFetchFinish() { + protected void handleActivityFetchFinish(boolean success) { LOG.info("Fetching activity data has finished round " + fetchCount); + // GregorianCalendar lastSyncTimestamp = saveSamples(); // if (lastSyncTimestamp != null && needsAnotherFetch(lastSyncTimestamp)) { // try { @@ -72,7 +91,31 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation { // } // } - super.handleActivityFetchFinish(); + BaseActivitySummary summary = null; + if (success) { + summary = parseSummary(buffer); + try (DBHandler dbHandler = GBApplication.acquireDB()) { + DaoSession session = dbHandler.getDaoSession(); + Device device = DBHelper.getDevice(getDevice(), session); + User user = DBHelper.getUser(session); + summary.setDevice(device); + summary.setUser(user); + session.getBaseActivitySummaryDao().insertOrReplace(summary); + } catch (Exception ex) { + GB.toast(getContext(), "Error saving activity summary", Toast.LENGTH_LONG, GB.ERROR, ex); + } + } + + super.handleActivityFetchFinish(success); + + if (summary != null) { + FetchSportsDetailsOperation nextOperation = new FetchSportsDetailsOperation(summary, getSupport()); + try { + nextOperation.perform(); + } catch (IOException ex) { + GB.toast(getContext(), "Unable to fetch activity details: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex); + } + } } @Override @@ -101,48 +144,103 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation { return; } - if ((value.length % 4) == 1) { - if ((byte) (lastPacketCounter + 1) == value[0] ) { - lastPacketCounter++; - bufferActivityData(value); - } else { - GB.toast("Error fetching activity data, invalid package counter: " + value[0], Toast.LENGTH_LONG, GB.ERROR); - handleActivityFetchFinish(); - return; - } + if (value.length < 2) { + LOG.error("unexpected sports summary data length: " + value.length); + getSupport().logMessageContent(value); + return; + } + + if ((byte) (lastPacketCounter + 1) == value[0] ) { + lastPacketCounter++; + bufferActivityData(value); } else { - GB.toast("Error fetching activity data, unexpected package length: " + value.length, Toast.LENGTH_LONG, GB.ERROR); - LOG.warn("Unexpected activity data: " + Logging.formatBytes(value)); + GB.toast("Error fetching activity data, invalid package counter: " + value[0] + ", last was: " + lastPacketCounter, Toast.LENGTH_LONG, GB.ERROR); + handleActivityFetchFinish(false); + return; } } /** - * Creates samples from the given 17-length array + * Buffers the given activity summary data. If the total size is reached, + * it is converted to an object and saved in the database. * @param value */ @Override protected void bufferActivityData(byte[] value) { - // TODO: implement -// int len = value.length; + buffer.write(value, 1, value.length - 1); // skip the counter + } + + private BaseActivitySummary parseSummary(ByteArrayOutputStream stream) { + BaseActivitySummary summary = new BaseActivitySummary(); + ByteBuffer buffer = ByteBuffer.wrap(stream.toByteArray()).order(ByteOrder.LITTLE_ENDIAN); +// summary.setVersion(BLETypeConversions.toUnsigned(buffer.getShort())); + buffer.getShort(); // version + int rawKind = BLETypeConversions.toUnsigned(buffer.getShort()); + int activityKind = MiBand2Const.toActivityKind(rawKind); + if (activityKind == ActivityKind.TYPE_UNKNOWN) { + activityKind = rawKind; // hack for later activity kind detection + } + summary.setActivityKind(activityKind); + // FIXME: should save timezone etc. + summary.setStartTime(new Date(BLETypeConversions.toUnsigned(buffer.getInt()) * 1000)); + summary.setEndTime(new Date(BLETypeConversions.toUnsigned(buffer.getInt()) * 1000)); + int baseLongitude = buffer.getInt(); + int baseLatitude = buffer.getInt(); + int baseAltitude = buffer.getInt(); + summary.setBaseLongitude(baseLongitude); + summary.setBaseLatitude(baseLatitude); + summary.setBaseAltitude(baseAltitude); +// summary.setBaseCoordinate(new GPSCoordinate(baseLatitude, baseLongitude, baseAltitude)); + +// summary.setDistanceMeters(Float.intBitsToFloat(buffer.getInt())); +// summary.setAscentMeters(Float.intBitsToFloat(buffer.getInt())); +// summary.setDescentMeters(Float.intBitsToFloat(buffer.getInt())); // -// if (len % 4 != 1) { -// throw new AssertionError("Unexpected activity array size: " + len); -// } +// summary.setMinAltitude(Float.intBitsToFloat(buffer.getInt())); +// summary.setMaxAltitude(Float.intBitsToFloat(buffer.getInt())); +// summary.setMinLatitude(buffer.getInt()); +// summary.setMaxLatitude(buffer.getInt()); +// summary.setMinLongitude(buffer.getInt()); +// summary.setMaxLongitude(buffer.getInt()); // -// for (int i = 1; i < len; i+=4) { -// } +// summary.setSteps(BLETypeConversions.toUnsigned(buffer.getInt())); +// summary.setActiveTimeSeconds(BLETypeConversions.toUnsigned(buffer.getInt())); +// +// summary.setCaloriesBurnt(Float.intBitsToFloat(buffer.get())); +// summary.setMaxSpeed(Float.intBitsToFloat(buffer.get())); +// summary.setMinPace(Float.intBitsToFloat(buffer.get())); +// summary.setMaxPace(Float.intBitsToFloat(buffer.get())); +// summary.setTotalStride(Float.intBitsToFloat(buffer.get())); + + buffer.getInt(); // + buffer.getInt(); // + buffer.getInt(); // + +// summary.setTimeAscent(BLETypeConversions.toUnsigned(buffer.getInt())); +// buffer.getInt(); // +// summary.setTimeDescent(BLETypeConversions.toUnsigned(buffer.getInt())); +// buffer.getInt(); // +// summary.setTimeFlat(BLETypeConversions.toUnsigned(buffer.getInt())); +// +// summary.setAverageHR(BLETypeConversions.toUnsigned(buffer.getShort())); +// +// summary.setAveragePace(BLETypeConversions.toUnsigned(buffer.getShort())); +// summary.setAverageStride(BLETypeConversions.toUnsigned(buffer.getShort())); + + buffer.getShort(); // + + return summary; } @Override protected String getLastSyncTimeKey() { - return getDevice().getAddress() + "_" + "lastSportsSyncTimeMillis"; + return getDevice().getAddress() + "_" + "lastSportsSummaryTimeMillis"; } - protected GregorianCalendar getLastSuccessfulSyncTime() { // FIXME: remove this! GregorianCalendar calendar = BLETypeConversions.createCalendar(); - calendar.add(Calendar.DAY_OF_MONTH, -1); + calendar.add(Calendar.DAY_OF_MONTH, -25); return calendar; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/AndroidUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/AndroidUtils.java index 5b9f83671..a2867badb 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/AndroidUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/AndroidUtils.java @@ -19,23 +19,36 @@ package nodomain.freeyourgadget.gadgetbridge.util; import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; import android.content.res.Configuration; import android.graphics.Color; +import android.net.Uri; import android.os.ParcelUuid; import android.os.Parcelable; +import android.support.v4.content.FileProvider; import android.support.v4.content.LocalBroadcastManager; +import java.io.File; +import java.io.IOException; import java.util.Locale; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.ActivitySummariesActivity; public class AndroidUtils { + /** + * Creates a new {@link ParcelUuid} array with the contents of the given uuids. + * The given array is expected to contain only {@link ParcelUuid} elements. + * @param uuids an array of {@link ParcelUuid} elements + * @return a {@link ParcelUuid} array instance with the same contents + */ public static ParcelUuid[] toParcelUUids(Parcelable[] uuids) { if (uuids == null) { return null; } ParcelUuid[] uuids2 = new ParcelUuid[uuids.length]; + //noinspection SuspiciousSystemArraycopy System.arraycopy(uuids, 0, uuids2, 0, uuids.length); return uuids2; } @@ -112,4 +125,15 @@ public class AndroidUtils { + Integer.toHexString(Color.green(color)) + Integer.toHexString(Color.blue(color)); } + + public static void viewFile(String path, String action, Context context) throws IOException { + Intent intent = new Intent(action); + File file = new File(path); + + Uri contentUri = FileProvider.getUriForFile(context, + context.getApplicationContext().getPackageName() + ".screenshot_provider", file); + intent.setFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.setData(contentUri); + context.startActivity(intent); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DateTimeUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DateTimeUtils.java index 068024b32..b5c3f8ac7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DateTimeUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DateTimeUtils.java @@ -33,9 +33,14 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication; public class DateTimeUtils { private static SimpleDateFormat DAY_STORAGE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.US); + public static SimpleDateFormat ISO_8601_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ssXXX", Locale.US); public static String formatDateTime(Date date) { - return DateUtils.formatDateTime(GBApplication.getContext(), date.getTime(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME); + return DateUtils.formatDateTime(GBApplication.getContext(), date.getTime(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_NO_YEAR); + } + + public static String formatIso8601(Date date) { + return ISO_8601_FORMAT.format(date); } public static String formatDate(Date date) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/FileUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/FileUtils.java index 117887341..63993f10a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/FileUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/FileUtils.java @@ -272,4 +272,14 @@ public class FileUtils { } throw new IOException("Cannot create temporary directory in " + parent); } + + /** + * Replaces some wellknown invalid characters in the given filename + * to underscrores. + * @param name the file name to make valid + * @return the valid file name + */ + public static String makeValidFileName(String name) { + return name.replaceAll("\0/:\\r\\n\\\\", "_"); + } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_activity_biking.xml b/app/src/main/res/drawable/ic_activity_biking.xml new file mode 100644 index 000000000..7001d100b --- /dev/null +++ b/app/src/main/res/drawable/ic_activity_biking.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_activity_deep_sleep.xml b/app/src/main/res/drawable/ic_activity_deep_sleep.xml new file mode 100644 index 000000000..abdaea833 --- /dev/null +++ b/app/src/main/res/drawable/ic_activity_deep_sleep.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_activity_light_sleep.xml b/app/src/main/res/drawable/ic_activity_light_sleep.xml new file mode 100644 index 000000000..abdaea833 --- /dev/null +++ b/app/src/main/res/drawable/ic_activity_light_sleep.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_activity_not_measured.xml b/app/src/main/res/drawable/ic_activity_not_measured.xml new file mode 100644 index 000000000..7a6bb2d01 --- /dev/null +++ b/app/src/main/res/drawable/ic_activity_not_measured.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_activity_running.xml b/app/src/main/res/drawable/ic_activity_running.xml new file mode 100644 index 000000000..14c06266a --- /dev/null +++ b/app/src/main/res/drawable/ic_activity_running.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_activity_tracks.xml b/app/src/main/res/drawable/ic_activity_tracks.xml new file mode 100644 index 000000000..14c06266a --- /dev/null +++ b/app/src/main/res/drawable/ic_activity_tracks.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_activity_unknown.xml b/app/src/main/res/drawable/ic_activity_unknown.xml new file mode 100644 index 000000000..4e323aeaa --- /dev/null +++ b/app/src/main/res/drawable/ic_activity_unknown.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_activity_walking.xml b/app/src/main/res/drawable/ic_activity_walking.xml new file mode 100644 index 000000000..831c0b345 --- /dev/null +++ b/app/src/main/res/drawable/ic_activity_walking.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_list.xml b/app/src/main/res/layout/activity_list.xml new file mode 100644 index 000000000..170019522 --- /dev/null +++ b/app/src/main/res/layout/activity_list.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/device_itemv2.xml b/app/src/main/res/layout/device_itemv2.xml index 517cd0444..09fcb4e20 100644 --- a/app/src/main/res/layout/device_itemv2.xml +++ b/app/src/main/res/layout/device_itemv2.xml @@ -231,12 +231,27 @@ card_view:srcCompat="@drawable/ic_activity_graphs" /> + + Open on phone Mute Reply + Your activity tracks + Not measured + Activity + Light sleep + Deep sleep + Device not worn + Running + Walking + Swimming + Unknown activity + Activities + Biking diff --git a/app/src/main/res/xml/screenshot_provider_paths.xml b/app/src/main/res/xml/shared_paths.xml similarity index 63% rename from app/src/main/res/xml/screenshot_provider_paths.xml rename to app/src/main/res/xml/shared_paths.xml index ffa74ab56..2d4b2d4c1 100644 --- a/app/src/main/res/xml/screenshot_provider_paths.xml +++ b/app/src/main/res/xml/shared_paths.xml @@ -1,4 +1,6 @@ + + \ No newline at end of file diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/ActivityDetailsParserTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/ActivityDetailsParserTest.java new file mode 100644 index 000000000..6b9fa25f2 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/ActivityDetailsParserTest.java @@ -0,0 +1,110 @@ +package nodomain.freeyourgadget.gadgetbridge.test; + +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Date; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip.BipActivitySummary; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.User; +import nodomain.freeyourgadget.gadgetbridge.export.GPXExporter; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack; +import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; +import nodomain.freeyourgadget.gadgetbridge.service.devices.amazfitbip.ActivityDetailsParser; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; +import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class ActivityDetailsParserTest extends TestBase { + private static final URL DETAILS_1 = ActivityDetailsParserTest.class.getClassLoader().getResource("ActivityDetailsDump1.txt"); + private static final long MAX_DETAILS = 1024 * 1024; + private static Date baseTime; + + @BeforeClass + public static void setUpSuite() throws Exception { + baseTime = DateTimeUtils.ISO_8601_FORMAT.parse("2017-01-20T14:00:00-00:00"); // yyyy-mm-dd'T'hh:mm:ssZ + } + + @Test + public void testActivityDetails() throws Exception { + BipActivitySummary summary = createSummary(); + + ActivityDetailsParser parser = new ActivityDetailsParser(summary); + parser.setSkipCounterByte(true); + try (InputStream in = getContents(DETAILS_1)) { + ActivityTrack track = parser.parse(FileUtils.readAll(in, MAX_DETAILS)); + assertEquals("SuperBand 2000", track.getDevice().getName()); + assertEquals("Elvis", track.getUser().getName()); + + List trackPoints = track.getTrackPoints(); + assertEquals(1208, trackPoints.size()); + } + } + + private BipActivitySummary createSummary() { + BipActivitySummary summary = new BipActivitySummary(); + summary.setBaseLongitude(1); + summary.setBaseLatitude(1); + summary.setBaseAltitude(1); + summary.setStartTime(baseTime); + User dummyUser = new User(0L); + dummyUser.setName("Elvis"); + summary.setName("testtrack"); + summary.setUser(dummyUser); + Device device = new Device(0l); + device.setName("SuperBand 2000"); + summary.setDevice(device); + + return summary; + } + + @Test + public void testGPXExport() throws Exception { + BipActivitySummary summary = createSummary(); + + int baseLongi = BLETypeConversions.toUint32((byte) 0xd6, (byte) 0xc4,(byte) 0x62,(byte) 0x02); + int baseLati = BLETypeConversions.toUint32((byte) 0xff, (byte) 0xa9, (byte) 0x61, (byte) 0x9); + int baseAlti = BLETypeConversions.toUint32((byte) 0x30, (byte) 0x0, (byte) 0x0, (byte) 0x0); + + summary.setBaseLongitude(baseLongi); + summary.setBaseLatitude(baseLati); + summary.setBaseAltitude(baseAlti); + + ActivityDetailsParser parser = new ActivityDetailsParser(summary); + parser.setSkipCounterByte(true); + try (InputStream in = getContents(DETAILS_1)) { + ActivityTrack track = parser.parse(FileUtils.readAll(in, MAX_DETAILS)); + + List trackPoints = track.getTrackPoints(); + assertEquals(1208, trackPoints.size()); + + + GPXExporter exporter = new GPXExporter(); + exporter.setIncludeHeartRate(false); + exporter.setCreator(getClass().getName()); + File targetFile = File.createTempFile("gadgetbridge-track", ".gpx"); + System.out.println("Writing GPX file: " + targetFile); + exporter.performExport(track, targetFile); + + assertTrue(targetFile.length() > 1024); + } + + } + + private InputStream getContents(URL hexFile) throws IOException { + return new HexToBinaryInputStream(hexFile.openStream()); + } + + +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/HexToBinaryInputStream.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/HexToBinaryInputStream.java new file mode 100644 index 000000000..da22ee0eb --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/HexToBinaryInputStream.java @@ -0,0 +1,52 @@ +package nodomain.freeyourgadget.gadgetbridge.test; + +import android.support.annotation.NonNull; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +class HexToBinaryInputStream extends FilterInputStream { + HexToBinaryInputStream(InputStream in) { + super(in); + } + + @Override + public int read() throws IOException { + int value; + StringBuilder buffer = new StringBuilder(4); + + loop: + while (true) { + value = super.read(); + switch (value) { + case -1: + case ' ': + case '\r': + case '\n': + break loop; + default: + buffer.append((char) value); + } + } + if (buffer.length() > 0) { + return Integer.decode(buffer.toString()); + } + return -1; + } + + @Override + public int read(@NonNull byte[] b, int off, int len) throws IOException { + if (off < 0 || len < 0 || len > b.length - off) { + throw new IndexOutOfBoundsException(); + } + for (int i = 0; i < len; i++) { + int value = read(); + if (value == -1) { + return i; + } + b[off + i] = (byte) value; + } + return len; + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/HexToBinaryInputStreamTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/HexToBinaryInputStreamTest.java new file mode 100644 index 000000000..a4b5f5b95 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/HexToBinaryInputStreamTest.java @@ -0,0 +1,50 @@ +package nodomain.freeyourgadget.gadgetbridge.test; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; + +import static org.junit.Assert.assertTrue; + +public class HexToBinaryInputStreamTest extends TestBase { + + @Test + public void testConversion() throws IOException { + byte[] hexString; + byte[] binString; + + try (InputStream in = ActivityDetailsParserTest.class.getClassLoader().getResourceAsStream("ActivityDetailsDump1.txt")) { + hexString = FileUtils.readAll(in, 1024 * 1024); + assertTrue(hexString.length > 1); + try (InputStream in2 = getContents(ActivityDetailsParserTest.class.getClassLoader().getResource("ActivityDetailsDump1.txt"))) { + binString = FileUtils.readAll(in2, 1024 * 1024); + assertTrue(binString.length > 1); + } + } + Assert.assertTrue(hexString.length > binString.length); + ByteArrayOutputStream binToHexOut = new ByteArrayOutputStream(hexString.length); + for (int i = 0; i < binString.length; i++) { + String hexed = String.format("0x%x", binString[i]); + binToHexOut.write(hexed.getBytes("US-ASCII")); + if ((i + 1) % 17 == 0) { + binToHexOut.write('\n'); + } else { + binToHexOut.write(' '); + } + } + + byte[] hexedBytes = binToHexOut.toByteArray(); + Assert.assertArrayEquals(hexString, hexedBytes); + } + + private InputStream getContents(URL hexFile) throws IOException { + return new HexToBinaryInputStream(hexFile.openStream()); + } + +} diff --git a/app/src/test/resources/ActivityDetailsDump1.txt b/app/src/test/resources/ActivityDetailsDump1.txt new file mode 100644 index 000000000..f5c33455a --- /dev/null +++ b/app/src/test/resources/ActivityDetailsDump1.txt @@ -0,0 +1,861 @@ +0x0 0x1 0x7 0x47 0x0 0x0 0x0 0x0 0x0 0x1 0x8 0x46 0x0 0x0 0x0 0x0 0x0 +0x1 0x1 0xa 0x43 0x0 0x0 0x0 0x0 0x0 0x1 0xc 0x46 0x0 0x0 0x0 0x0 0x0 +0x2 0x1 0xd 0x44 0x0 0x0 0x0 0x0 0x0 0x1 0xf 0x3e 0x0 0x0 0x0 0x0 0x0 +0x3 0x1 0x11 0x3b 0x0 0x0 0x0 0x0 0x0 0x1 0x12 0x3e 0x0 0x0 0x0 0x0 0x0 +0x4 0x1 0x17 0x3d 0x0 0x0 0x0 0x0 0x0 0x1 0x18 0x3f 0x0 0x0 0x0 0x0 0x0 +0x5 0x1 0x1b 0x41 0x0 0x0 0x0 0x0 0x0 0x1 0x1d 0x42 0x0 0x0 0x0 0x0 0x0 +0x6 0x1 0x20 0x43 0x0 0x0 0x0 0x0 0x0 0x1 0x21 0x44 0x0 0x0 0x0 0x0 0x0 +0x7 0x1 0x22 0x45 0x0 0x0 0x0 0x0 0x0 0x0 0x23 0x0 0x0 0x0 0x0 0x0 0x0 +0x8 0x0 0x23 0x79 0xff 0xcd 0x0 0x0 0x0 0x0 0x23 0x88 0xff 0xc3 0x0 0x0 0x0 +0x9 0x0 0x23 0xbf 0xff 0xeb 0x0 0x0 0x0 0x0 0x23 0xec 0xff 0x4 0x1 0x0 0x0 +0xa 0x0 0x23 0x5 0x0 0x4 0x1 0x0 0x0 0x0 0x23 0xf6 0xff 0x4f 0x1 0x0 0x0 +0xb 0x0 0x23 0xec 0xff 0x18 0x1 0x0 0x0 0x0 0x23 0xf1 0xff 0xff 0x0 0x0 0x0 +0xc 0x0 0x23 0xf1 0xff 0xf5 0x0 0x0 0x0 0x0 0x23 0xe2 0xff 0xa0 0x0 0x0 0x0 +0xd 0x0 0x23 0x9c 0xff 0xeb 0x0 0x0 0x0 0x0 0x23 0xf1 0xff 0xeb 0x0 0x0 0x0 +0xe 0x0 0x23 0xfb 0xff 0xe 0x1 0x0 0x0 0x0 0x23 0xf6 0xff 0x13 0x1 0x0 0x0 +0xf 0x0 0x23 0xfb 0xff 0x68 0x1 0x0 0x0 0x0 0x23 0xe2 0xff 0x18 0x1 0x0 0x0 +0x10 0x0 0x23 0xb5 0xff 0xd2 0x0 0x0 0x0 0x0 0x23 0x97 0xff 0x87 0x0 0x0 0x0 +0x11 0x0 0x23 0xba 0xff 0xa5 0x0 0x0 0x0 0x0 0x23 0xe2 0xff 0xfa 0x0 0x0 0x0 +0x12 0x0 0x23 0xe2 0xff 0xb9 0x0 0x0 0x0 0x0 0x23 0xfb 0xff 0x4 0x1 0x0 0x0 +0x13 0x0 0x23 0xec 0xff 0xa5 0x0 0x0 0x0 0x0 0x23 0xf1 0xff 0x2c 0x1 0x0 0x0 +0x14 0x0 0x23 0x5 0x0 0xe 0x1 0x0 0x0 0x0 0x23 0xec 0xff 0x9b 0x0 0x0 0x0 +0x15 0x0 0x23 0xf1 0xff 0xa5 0x0 0x0 0x0 0x0 0x23 0xf6 0xff 0xb4 0x0 0x0 0x0 +0x16 0x0 0x23 0xec 0xff 0xc3 0x0 0x0 0x0 0x0 0x23 0x7e 0xff 0xd2 0x0 0x0 0x0 +0x17 0x6 0x23 0x81 0x0 0xd2 0x0 0x0 0x0 0x0 0x24 0xd3 0xff 0x7d 0x0 0x0 0x0 +0x18 0x6 0x24 0x81 0x0 0x7d 0x0 0x0 0x0 0x0 0x25 0xc1 0xff 0xb1 0x0 0x0 0x0 +0x19 0x6 0x25 0x81 0x0 0xb1 0x0 0x0 0x0 0x0 0x26 0xca 0xff 0xc9 0x0 0x0 0x0 +0x1a 0x6 0x26 0x81 0x0 0xc9 0x0 0x0 0x0 0x0 0x27 0xd7 0xff 0xdd 0x0 0x0 0x0 +0x1b 0x6 0x27 0x81 0x0 0xdd 0x0 0x0 0x0 0x1 0x27 0x46 0x0 0x0 0x0 0x0 0x0 +0x1c 0x0 0x28 0xe4 0xff 0xf8 0x0 0x0 0x0 0x6 0x28 0x81 0x0 0xf8 0x0 0x0 0x0 +0x1d 0x0 0x29 0xef 0xff 0xf 0x1 0x0 0x0 0x6 0x29 0x81 0x0 0xf 0x1 0x0 0x0 +0x1e 0x1 0x29 0x43 0x0 0x0 0x0 0x0 0x0 0x0 0x2a 0xf1 0xff 0x2 0x1 0x0 0x0 +0x1f 0x6 0x2a 0x81 0x0 0x2 0x1 0x0 0x0 0x1 0x2a 0x45 0x0 0x0 0x0 0x0 0x0 +0x20 0x0 0x2b 0xeb 0xff 0xf4 0x0 0xfe 0xff 0x6 0x2b 0x81 0x0 0xf4 0x0 0xfe 0xff +0x21 0x0 0x2c 0xe8 0xff 0xdc 0x0 0x0 0x0 0x6 0x2c 0x81 0x0 0xdc 0x0 0x0 0x0 +0x22 0x0 0x2d 0xe9 0xff 0xca 0x0 0x0 0x0 0x6 0x2d 0x81 0x0 0xca 0x0 0x0 0x0 +0x23 0x0 0x2e 0xeb 0xff 0xc9 0x0 0x0 0x0 0x6 0x2e 0x81 0x0 0xc9 0x0 0x0 0x0 +0x24 0x1 0x2e 0x46 0x0 0x0 0x0 0x0 0x0 0x0 0x2f 0xed 0xff 0xcb 0x0 0x0 0x0 +0x25 0x6 0x2f 0x81 0x0 0xcb 0x0 0x0 0x0 0x1 0x2f 0x47 0x0 0x0 0x0 0x0 0x0 +0x26 0x0 0x30 0xf0 0xff 0xc5 0x0 0x0 0x0 0x6 0x30 0x81 0x0 0xc5 0x0 0x0 0x0 +0x27 0x0 0x31 0xf2 0xff 0xb7 0x0 0x0 0x0 0x6 0x31 0x81 0x0 0xb7 0x0 0x0 0x0 +0x28 0x0 0x32 0xf1 0xff 0xb1 0x0 0x0 0x0 0x6 0x32 0x81 0x0 0xb1 0x0 0x0 0x0 +0x29 0x0 0x33 0xf7 0xff 0xb5 0x0 0x0 0x0 0x6 0x33 0x81 0x0 0xb5 0x0 0x0 0x0 +0x2a 0x1 0x33 0x49 0x0 0x0 0x0 0x0 0x0 0x0 0x34 0xfa 0xff 0xc8 0x0 0x0 0x0 +0x2b 0x6 0x34 0x81 0x0 0xc8 0x0 0x0 0x0 0x1 0x34 0x4a 0x0 0x0 0x0 0x0 0x0 +0x2c 0x0 0x35 0xf7 0xff 0xda 0x0 0x0 0x0 0x6 0x35 0x81 0x0 0xda 0x0 0x0 0x0 +0x2d 0x1 0x35 0x4b 0x0 0x0 0x0 0x0 0x0 0x0 0x36 0xf3 0xff 0xdb 0x0 0x0 0x0 +0x2e 0x6 0x36 0x81 0x0 0xdb 0x0 0x0 0x0 0x0 0x37 0xe9 0xff 0xed 0x0 0x0 0x0 +0x2f 0x6 0x37 0x81 0x0 0xed 0x0 0x0 0x0 0x1 0x38 0x4c 0x0 0x0 0x0 0x0 0x0 +0x30 0x0 0x38 0xe9 0xff 0xf6 0x0 0x0 0x0 0x6 0x38 0x81 0x0 0xf6 0x0 0x0 0x0 +0x31 0x0 0x39 0xeb 0xff 0xe4 0x0 0xfe 0xff 0x6 0x39 0x81 0x0 0xe4 0x0 0xfe 0xff +0x32 0x1 0x39 0x4d 0x0 0x0 0x0 0x0 0x0 0x0 0x3a 0xef 0xff 0xbd 0x0 0x0 0x0 +0x33 0x6 0x3a 0x81 0x0 0xbd 0x0 0x0 0x0 0x1 0x3a 0x4e 0x0 0x0 0x0 0x0 0x0 +0x34 0x0 0x3c 0xed 0xff 0xcc 0x0 0x0 0x0 0x6 0x3c 0x81 0x0 0xcc 0x0 0x0 0x0 +0x35 0x1 0x3d 0x4f 0x0 0x0 0x0 0x0 0x0 0x1 0x3e 0x50 0x0 0x0 0x0 0x0 0x0 +0x36 0x0 0x3e 0x1 0x0 0xcd 0x0 0x0 0x0 0x6 0x3e 0x81 0x0 0xcd 0x0 0x0 0x0 +0x37 0x0 0x3f 0x14 0x0 0x91 0x0 0x0 0x0 0x6 0x3f 0x81 0x0 0x91 0x0 0x0 0x0 +0x38 0x1 0x3f 0x51 0x0 0x0 0x0 0x0 0x0 0x0 0x41 0x86 0x1 0x6e 0x0 0x0 0x0 +0x39 0x6 0x41 0x81 0x0 0x6e 0x0 0x0 0x0 0x1 0x41 0x52 0x0 0x0 0x0 0x0 0x0 +0x3a 0x0 0x42 0x5e 0x1 0x55 0x0 0x0 0x0 0x6 0x42 0x81 0x0 0x55 0x0 0x0 0x0 +0x3b 0x0 0x43 0x36 0x1 0x48 0x0 0x0 0x0 0x6 0x43 0x81 0x0 0x48 0x0 0x0 0x0 +0x3c 0x0 0x44 0x31 0x1 0x36 0x0 0x0 0x0 0x6 0x44 0x81 0x0 0x36 0x0 0x0 0x0 +0x3d 0x1 0x44 0x53 0x0 0x0 0x0 0x0 0x0 0x0 0x45 0x3d 0x1 0x2f 0x0 0x0 0x0 +0x3e 0x6 0x45 0x81 0x0 0x2f 0x0 0x0 0x0 0x0 0x46 0x35 0x1 0x38 0x0 0x0 0x0 +0x3f 0x6 0x46 0x81 0x0 0x38 0x0 0x0 0x0 0x1 0x46 0x54 0x0 0x0 0x0 0x0 0x0 +0x40 0x0 0x47 0x76 0x1 0x37 0x0 0x0 0x0 0x6 0x47 0x81 0x0 0x37 0x0 0x0 0x0 +0x41 0x0 0x48 0xbd 0x1 0x32 0x0 0x0 0x0 0x6 0x48 0x81 0x0 0x32 0x0 0x0 0x0 +0x42 0x1 0x49 0x55 0x0 0x0 0x0 0x0 0x0 0x0 0x49 0xb0 0x1 0xd 0x0 0x0 0x0 +0x43 0x6 0x49 0x81 0x0 0xd 0x0 0x0 0x0 0x0 0x4a 0x9b 0x1 0x1a 0x0 0x0 0x0 +0x44 0x6 0x4a 0x81 0x0 0x1a 0x0 0x0 0x0 0x0 0x4b 0x7a 0x1 0x1c 0x0 0xfe 0xff +0x45 0x6 0x4b 0x81 0x0 0x1c 0x0 0xfe 0xff 0x0 0x4c 0x53 0x1 0x34 0x0 0x0 0x0 +0x46 0x6 0x4c 0x81 0x0 0x34 0x0 0x0 0x0 0x1 0x4c 0x56 0x0 0x0 0x0 0x0 0x0 +0x47 0x0 0x4d 0x24 0x1 0xfe 0xff 0x0 0x0 0x6 0x4d 0x81 0x0 0xfe 0xff 0x0 0x0 +0x48 0x1 0x4e 0x55 0x0 0x0 0x0 0x0 0x0 0x0 0x4f 0x3c 0x1 0xff 0xff 0x0 0x0 +0x49 0x6 0x4f 0x81 0x0 0xff 0xff 0x0 0x0 0x0 0x51 0x5 0x1 0x2a 0x0 0x0 0x0 +0x4a 0x6 0x51 0x81 0x0 0x2a 0x0 0x0 0x0 0x1 0x52 0x54 0x0 0x0 0x0 0x0 0x0 +0x4b 0x0 0x53 0xd5 0x0 0x77 0x0 0x0 0x0 0x6 0x53 0x81 0x0 0x77 0x0 0x0 0x0 +0x4c 0x0 0x54 0x86 0x0 0x89 0x0 0x0 0x0 0x6 0x54 0x81 0x0 0x89 0x0 0x0 0x0 +0x4d 0x1 0x55 0x53 0x0 0x0 0x0 0x0 0x0 0x0 0x55 0x59 0x0 0xa3 0x0 0x0 0x0 +0x4e 0x6 0x55 0x95 0x0 0xa3 0x0 0x0 0x0 0x0 0x56 0x3c 0x0 0xc2 0x0 0x0 0x0 +0x4f 0x6 0x56 0x95 0x0 0xc2 0x0 0x0 0x0 0x1 0x56 0x4f 0x0 0x0 0x0 0x0 0x0 +0x50 0x0 0x57 0x23 0x0 0xe8 0x0 0x0 0x0 0x6 0x57 0x95 0x0 0xe8 0x0 0x0 0x0 +0x51 0x1 0x57 0x50 0x0 0x0 0x0 0x0 0x0 0x0 0x58 0x11 0x0 0x5 0x1 0x0 0x0 +0x52 0x6 0x58 0x95 0x0 0x5 0x1 0x0 0x0 0x0 0x59 0xfc 0xff 0xf 0x1 0x0 0x0 +0x53 0x6 0x59 0x95 0x0 0xf 0x1 0x0 0x0 0x1 0x5a 0x51 0x0 0x0 0x0 0x0 0x0 +0x54 0x0 0x5a 0xed 0xff 0x25 0x1 0x0 0x0 0x6 0x5a 0x95 0x0 0x25 0x1 0x0 0x0 +0x55 0x0 0x5b 0xe7 0xff 0x32 0x1 0x0 0x0 0x6 0x5b 0x95 0x0 0x32 0x1 0x0 0x0 +0x56 0x0 0x5c 0xb8 0xff 0xa5 0x1 0x0 0x0 0x6 0x5c 0x95 0x0 0xa5 0x1 0x0 0x0 +0x57 0x0 0x5d 0xc0 0xff 0x7c 0x1 0x0 0x0 0x6 0x5d 0x95 0x0 0x7c 0x1 0x0 0x0 +0x58 0x0 0x5e 0xde 0xff 0x3e 0x1 0x0 0x0 0x6 0x5e 0x82 0x0 0x3e 0x1 0x0 0x0 +0x59 0x1 0x5f 0x52 0x0 0x0 0x0 0x0 0x0 0x0 0x5f 0xd8 0xff 0x46 0x1 0x0 0x0 +0x5a 0x6 0x5f 0x82 0x0 0x46 0x1 0x0 0x0 0x1 0x60 0x53 0x0 0x0 0x0 0x0 0x0 +0x5b 0x0 0x60 0xd3 0xff 0x39 0x1 0x0 0x0 0x6 0x60 0x82 0x0 0x39 0x1 0x0 0x0 +0x5c 0x0 0x61 0xc9 0xff 0x1f 0x1 0x0 0x0 0x6 0x61 0x82 0x0 0x1f 0x1 0x0 0x0 +0x5d 0x1 0x61 0x55 0x0 0x0 0x0 0x0 0x0 0x0 0x63 0xbf 0xff 0xdd 0x0 0x0 0x0 +0x5e 0x6 0x63 0x82 0x0 0xdd 0x0 0x0 0x0 0x1 0x63 0x56 0x0 0x0 0x0 0x0 0x0 +0x5f 0x1 0x66 0x57 0x0 0x0 0x0 0x0 0x0 0x0 0x6a 0xea 0xff 0xc6 0x0 0x0 0x0 +0x60 0x6 0x6a 0x82 0x0 0xc6 0x0 0x0 0x0 0x1 0x6b 0x56 0x0 0x0 0x0 0x0 0x0 +0x61 0x0 0x6c 0x59 0x0 0x9f 0x0 0x0 0x0 0x6 0x6c 0x96 0x0 0x9f 0x0 0x0 0x0 +0x62 0x1 0x6d 0x55 0x0 0x0 0x0 0x0 0x0 0x0 0x6e 0x24 0x1 0x61 0x0 0x0 0x0 +0x63 0x6 0x6e 0x96 0x0 0x61 0x0 0x0 0x0 0x1 0x6e 0x54 0x0 0x0 0x0 0x0 0x0 +0x64 0x0 0x6f 0x39 0x1 0x24 0x0 0x0 0x0 0x6 0x6f 0x96 0x0 0x24 0x0 0x0 0x0 +0x65 0x1 0x70 0x53 0x0 0x0 0x0 0x0 0x0 0x0 0x70 0x5b 0x1 0xfb 0xff 0x0 0x0 +0x66 0x6 0x70 0x96 0x0 0xfb 0xff 0x0 0x0 0x0 0x71 0x7f 0x1 0xe0 0xff 0x0 0x0 +0x67 0x6 0x71 0x96 0x0 0xe0 0xff 0x0 0x0 0x0 0x72 0xa3 0x1 0xd4 0xff 0x0 0x0 +0x68 0x6 0x72 0x96 0x0 0xd4 0xff 0x0 0x0 0x1 0x72 0x54 0x0 0x0 0x0 0x0 0x0 +0x69 0x0 0x73 0xc2 0x1 0xcf 0xff 0x0 0x0 0x6 0x73 0x96 0x0 0xcf 0xff 0x0 0x0 +0x6a 0x0 0x74 0xd8 0x1 0xc7 0xff 0x0 0x0 0x6 0x74 0x96 0x0 0xc7 0xff 0x0 0x0 +0x6b 0x1 0x74 0x55 0x0 0x0 0x0 0x0 0x0 0x0 0x75 0xe6 0x1 0xbe 0xff 0x0 0x0 +0x6c 0x6 0x75 0x96 0x0 0xbe 0xff 0x0 0x0 0x0 0x76 0xeb 0x1 0xb2 0xff 0x0 0x0 +0x6d 0x6 0x76 0x96 0x0 0xb2 0xff 0x0 0x0 0x1 0x77 0x56 0x0 0x0 0x0 0x0 0x0 +0x6e 0x0 0x77 0x8a 0x2 0x70 0xff 0x0 0x0 0x6 0x77 0x96 0x0 0x70 0xff 0x0 0x0 +0x6f 0x0 0x78 0x92 0x2 0x9a 0xff 0x0 0x0 0x6 0x78 0x96 0x0 0x9a 0xff 0x0 0x0 +0x70 0x1 0x78 0x57 0x0 0x0 0x0 0x0 0x0 0x0 0x79 0x7c 0x2 0xb9 0xff 0x0 0x0 +0x71 0x6 0x79 0x96 0x0 0xb9 0xff 0x0 0x0 0x0 0x7a 0x59 0x2 0xb9 0xff 0x0 0x0 +0x72 0x6 0x7a 0x83 0x0 0xb9 0xff 0x0 0x0 0x0 0x7b 0x33 0x2 0xa3 0xff 0x0 0x0 +0x73 0x6 0x7b 0x83 0x0 0xa3 0xff 0x0 0x0 0x0 0x7c 0x27 0x2 0xac 0xff 0x0 0x0 +0x74 0x6 0x7c 0x83 0x0 0xac 0xff 0x0 0x0 0x1 0x7d 0x56 0x0 0x0 0x0 0x0 0x0 +0x75 0x0 0x7d 0x29 0x2 0xb1 0xff 0x0 0x0 0x6 0x7d 0x83 0x0 0xb1 0xff 0x0 0x0 +0x76 0x0 0x7e 0x16 0x2 0xa9 0xff 0x2 0x0 0x6 0x7e 0x83 0x0 0xa9 0xff 0x2 0x0 +0x77 0x1 0x7e 0x55 0x0 0x0 0x0 0x0 0x0 0x0 0x7f 0xd 0x2 0xaf 0xff 0x0 0x0 +0x78 0x6 0x7f 0x83 0x0 0xaf 0xff 0x0 0x0 0x0 0x80 0x0 0x2 0xb5 0xff 0x0 0x0 +0x79 0x6 0x80 0x70 0x0 0xb5 0xff 0x0 0x0 0x1 0x80 0x54 0x0 0x0 0x0 0x0 0x0 +0x7a 0x0 0x81 0x7 0x2 0xb7 0xff 0x0 0x0 0x6 0x81 0x70 0x0 0xb7 0xff 0x0 0x0 +0x7b 0x0 0x82 0xa 0x2 0xc2 0xff 0x0 0x0 0x6 0x82 0x70 0x0 0xc2 0xff 0x0 0x0 +0x7c 0x4 0x83 0x83 0x0 0x15 0x0 0x0 0x0 0x0 0x83 0x6 0x2 0xc6 0xff 0x0 0x0 +0x7d 0x6 0x83 0x70 0x0 0xc6 0xff 0x0 0x0 0x0 0x84 0x0 0x2 0xc2 0xff 0x0 0x0 +0x7e 0x6 0x84 0x70 0x0 0xc2 0xff 0x0 0x0 0x0 0x85 0x9 0x2 0xba 0xff 0x0 0x0 +0x7f 0x6 0x85 0x70 0x0 0xba 0xff 0x0 0x0 0x1 0x85 0x54 0x0 0x0 0x0 0x0 0x0 +0x80 0x0 0x86 0x8 0x2 0xb6 0xff 0x0 0x0 0x6 0x86 0x70 0x0 0xb6 0xff 0x0 0x0 +0x81 0x0 0x87 0xfe 0x1 0xaf 0xff 0x0 0x0 0x6 0x87 0x70 0x0 0xaf 0xff 0x0 0x0 +0x82 0x1 0x88 0x55 0x0 0x0 0x0 0x0 0x0 0x0 0x88 0xfc 0x1 0xae 0xff 0x0 0x0 +0x83 0x6 0x88 0x70 0x0 0xae 0xff 0x0 0x0 0x0 0x89 0xf3 0x1 0xad 0xff 0x0 0x0 +0x84 0x6 0x89 0x70 0x0 0xad 0xff 0x0 0x0 0x0 0x8a 0xe4 0x1 0xae 0xff 0x0 0x0 +0x85 0x6 0x8a 0x70 0x0 0xae 0xff 0x0 0x0 0x1 0x8a 0x56 0x0 0x0 0x0 0x0 0x0 +0x86 0x0 0x8b 0xd3 0x1 0xad 0xff 0x0 0x0 0x6 0x8b 0x70 0x0 0xad 0xff 0x0 0x0 +0x87 0x0 0x8c 0xcd 0x1 0xb3 0xff 0x0 0x0 0x6 0x8c 0x70 0x0 0xb3 0xff 0x0 0x0 +0x88 0x1 0x8d 0x57 0x0 0x0 0x0 0x0 0x0 0x0 0x8d 0xcf 0x1 0xae 0xff 0x0 0x0 +0x89 0x6 0x8d 0x70 0x0 0xae 0xff 0x0 0x0 0x0 0x8e 0xdf 0x1 0xb0 0xff 0x0 0x0 +0x8a 0x6 0x8e 0x70 0x0 0xb0 0xff 0x0 0x0 0x0 0x8f 0xec 0x1 0xb9 0xff 0x2 0x0 +0x8b 0x6 0x8f 0x70 0x0 0xb9 0xff 0x2 0x0 0x0 0x90 0xfa 0x1 0xb5 0xff 0x0 0x0 +0x8c 0x6 0x90 0x70 0x0 0xb5 0xff 0x0 0x0 0x1 0x90 0x59 0x0 0x0 0x0 0x0 0x0 +0x8d 0x0 0x91 0x1 0x2 0xcd 0xff 0x0 0x0 0x6 0x91 0x70 0x0 0xcd 0xff 0x0 0x0 +0x8e 0x0 0x92 0xa 0x2 0xda 0xff 0x0 0x0 0x6 0x92 0x70 0x0 0xda 0xff 0x0 0x0 +0x8f 0x0 0x93 0xe 0x2 0xd7 0xff 0x0 0x0 0x6 0x93 0x70 0x0 0xd7 0xff 0x0 0x0 +0x90 0x1 0x94 0x5a 0x0 0x0 0x0 0x0 0x0 0x0 0x94 0x15 0x2 0xda 0xff 0x0 0x0 +0x91 0x6 0x94 0x70 0x0 0xda 0xff 0x0 0x0 0x0 0x95 0x13 0x2 0xed 0xff 0x0 0x0 +0x92 0x6 0x95 0x70 0x0 0xed 0xff 0x0 0x0 0x1 0x95 0x5b 0x0 0x0 0x0 0x0 0x0 +0x93 0x0 0x96 0xe1 0x1 0xf4 0xff 0x0 0x0 0x6 0x96 0x70 0x0 0xf4 0xff 0x0 0x0 +0x94 0x1 0x96 0x5c 0x0 0x0 0x0 0x0 0x0 0x0 0x97 0x38 0x2 0x4 0x0 0x0 0x0 +0x95 0x6 0x97 0x70 0x0 0x4 0x0 0x0 0x0 0x0 0x98 0x2f 0x2 0x13 0x0 0x0 0x0 +0x96 0x6 0x98 0x5d 0x0 0x13 0x0 0x0 0x0 0x1 0x99 0x5d 0x0 0x0 0x0 0x0 0x0 +0x97 0x0 0x99 0x2a 0x2 0x23 0x0 0x0 0x0 0x6 0x99 0x5d 0x0 0x23 0x0 0x0 0x0 +0x98 0x0 0x9a 0x28 0x2 0x25 0x0 0x0 0x0 0x6 0x9a 0x5d 0x0 0x25 0x0 0x0 0x0 +0x99 0x1 0x9a 0x5e 0x0 0x0 0x0 0x0 0x0 0x0 0x9b 0x22 0x2 0x2c 0x0 0x0 0x0 +0x9a 0x6 0x9b 0x5d 0x0 0x2c 0x0 0x0 0x0 0x1 0x9b 0x5f 0x0 0x0 0x0 0x0 0x0 +0x9b 0x0 0x9c 0x18 0x2 0x34 0x0 0x0 0x0 0x6 0x9c 0x5d 0x0 0x34 0x0 0x0 0x0 +0x9c 0x0 0x9d 0xe3 0x1 0x38 0x0 0x0 0x0 0x6 0x9d 0x5d 0x0 0x38 0x0 0x0 0x0 +0x9d 0x1 0x9e 0x60 0x0 0x0 0x0 0x0 0x0 0x0 0x9e 0x2f 0x2 0x50 0x0 0x0 0x0 +0x9e 0x6 0x9e 0x5d 0x0 0x50 0x0 0x0 0x0 0x1 0x9f 0x61 0x0 0x0 0x0 0x0 0x0 +0x9f 0x0 0x9f 0x23 0x2 0x5a 0x0 0x0 0x0 0x6 0x9f 0x5d 0x0 0x5a 0x0 0x0 0x0 +0xa0 0x0 0xa0 0x1d 0x2 0x6c 0x0 0x0 0x0 0x6 0xa0 0x5d 0x0 0x6c 0x0 0x0 0x0 +0xa1 0x1 0xa0 0x60 0x0 0x0 0x0 0x0 0x0 0x0 0xa1 0x15 0x2 0x72 0x0 0x0 0x0 +0xa2 0x6 0xa1 0x5d 0x0 0x72 0x0 0x0 0x0 0x0 0xa2 0xe 0x2 0x7f 0x0 0x0 0x0 +0xa3 0x6 0xa2 0x5d 0x0 0x7f 0x0 0x0 0x0 0x0 0xa3 0x6 0x2 0x88 0x0 0x0 0x0 +0xa4 0x6 0xa3 0x5d 0x0 0x88 0x0 0x0 0x0 0x1 0xa4 0x61 0x0 0x0 0x0 0x0 0x0 +0xa5 0x0 0xa4 0xfd 0x1 0x96 0x0 0x0 0x0 0x6 0xa4 0x5d 0x0 0x96 0x0 0x0 0x0 +0xa6 0x0 0xa5 0xc2 0x1 0x91 0x0 0x0 0x0 0x6 0xa5 0x5d 0x0 0x91 0x0 0x0 0x0 +0xa7 0x0 0xa6 0x0 0x2 0xaf 0x0 0x0 0x0 0x6 0xa6 0x5d 0x0 0xaf 0x0 0x0 0x0 +0xa8 0x0 0xa7 0xe4 0x1 0xb2 0x0 0x0 0x0 0x6 0xa7 0x5d 0x0 0xb2 0x0 0x0 0x0 +0xa9 0x1 0xa7 0x62 0x0 0x0 0x0 0x0 0x0 0x0 0xa8 0xc9 0x1 0xb3 0x0 0x0 0x0 +0xaa 0x6 0xa8 0x5d 0x0 0xb3 0x0 0x0 0x0 0x1 0xa9 0x61 0x0 0x0 0x0 0x0 0x0 +0xab 0x0 0xa9 0xaf 0x1 0xb6 0x0 0x2 0x0 0x6 0xa9 0x5d 0x0 0xb6 0x0 0x2 0x0 +0xac 0x0 0xaa 0xa2 0x1 0xbc 0x0 0x0 0x0 0x6 0xaa 0x5d 0x0 0xbc 0x0 0x0 0x0 +0xad 0x0 0xab 0x69 0x1 0xae 0x0 0x0 0x0 0x6 0xab 0x5d 0x0 0xae 0x0 0x0 0x0 +0xae 0x0 0xac 0x93 0x1 0xcf 0x0 0x0 0x0 0x6 0xac 0x5d 0x0 0xcf 0x0 0x0 0x0 +0xaf 0x1 0xac 0x62 0x0 0x0 0x0 0x0 0x0 0x0 0xad 0x4c 0x1 0xb8 0x0 0x0 0x0 +0xb0 0x6 0xad 0x5d 0x0 0xb8 0x0 0x0 0x0 0x1 0xad 0x61 0x0 0x0 0x0 0x0 0x0 +0xb1 0x0 0xae 0x43 0x1 0xc0 0x0 0x0 0x0 0x6 0xae 0x5d 0x0 0xc0 0x0 0x0 0x0 +0xb2 0x0 0xaf 0x60 0x1 0xdd 0x0 0x0 0x0 0x6 0xaf 0x5d 0x0 0xdd 0x0 0x0 0x0 +0xb3 0x0 0xb0 0x48 0x1 0xd5 0x0 0x0 0x0 0x6 0xb0 0x5d 0x0 0xd5 0x0 0x0 0x0 +0xb4 0x0 0xb1 0x3f 0x1 0xe0 0x0 0x0 0x0 0x6 0xb1 0x5d 0x0 0xe0 0x0 0x0 0x0 +0xb5 0x0 0xb2 0x1a 0x1 0xd4 0x0 0x0 0x0 0x6 0xb2 0x5d 0x0 0xd4 0x0 0x0 0x0 +0xb6 0x1 0xb2 0x61 0x0 0x0 0x0 0x0 0x0 0x0 0xb3 0x26 0x1 0xe7 0x0 0x0 0x0 +0xb7 0x6 0xb3 0x5d 0x0 0xe7 0x0 0x0 0x0 0x0 0xb4 0x47 0x1 0x4 0x1 0x0 0x0 +0xb8 0x6 0xb4 0x5d 0x0 0x4 0x1 0x0 0x0 0x0 0xb5 0x3c 0x1 0xff 0x0 0x0 0x0 +0xb9 0x6 0xb5 0x5d 0x0 0xff 0x0 0x0 0x0 0x1 0xb6 0x60 0x0 0x0 0x0 0x0 0x0 +0xba 0x0 0xb6 0x2f 0x1 0xf8 0x0 0x0 0x0 0x6 0xb6 0x5d 0x0 0xf8 0x0 0x0 0x0 +0xbb 0x0 0xb7 0x32 0x1 0xf9 0x0 0x0 0x0 0x6 0xb7 0x5d 0x0 0xf9 0x0 0x0 0x0 +0xbc 0x5 0xb7 0xb7 0x0 0xf9 0x0 0x0 0x0 0x1 0xb7 0x61 0x0 0x0 0x0 0x0 0x0 +0xbd 0x0 0xb8 0x32 0x1 0xf2 0x0 0x0 0x0 0x6 0xb8 0x5d 0x0 0xf2 0x0 0x0 0x0 +0xbe 0x0 0xb9 0x25 0x1 0xee 0x0 0x0 0x0 0x6 0xb9 0x5d 0x0 0xee 0x0 0x0 0x0 +0xbf 0x0 0xba 0x14 0x1 0xe3 0x0 0x0 0x0 0x6 0xba 0x5d 0x0 0xe3 0x0 0x0 0x0 +0xc0 0x1 0xbb 0x60 0x0 0x0 0x0 0x0 0x0 0x0 0xbb 0x0 0x1 0xd3 0x0 0x0 0x0 +0xc1 0x6 0xbb 0x5d 0x0 0xd3 0x0 0x0 0x0 0x0 0xbc 0xe4 0x0 0xbe 0x0 0x0 0x0 +0xc2 0x6 0xbc 0x5d 0x0 0xbe 0x0 0x0 0x0 0x1 0xbc 0x61 0x0 0x0 0x0 0x0 0x0 +0xc3 0x0 0xbd 0xc7 0x0 0xaa 0x0 0x0 0x0 0x6 0xbd 0x5d 0x0 0xaa 0x0 0x0 0x0 +0xc4 0x1 0xbd 0x62 0x0 0x0 0x0 0x0 0x0 0x0 0xbe 0xa0 0x0 0x8c 0x0 0x0 0x0 +0xc5 0x6 0xbe 0x5d 0x0 0x8c 0x0 0x0 0x0 0x0 0xbf 0xbc 0x0 0xa3 0x0 0x0 0x0 +0xc6 0x6 0xbf 0x5d 0x0 0xa3 0x0 0x0 0x0 0x1 0xc0 0x63 0x0 0x0 0x0 0x0 0x0 +0xc7 0x0 0xc0 0xbf 0x0 0xa4 0x0 0x0 0x0 0x6 0xc0 0x5d 0x0 0xa4 0x0 0x0 0x0 +0xc8 0x0 0xc1 0xbe 0x0 0x9f 0x0 0x0 0x0 0x6 0xc1 0x5d 0x0 0x9f 0x0 0x0 0x0 +0xc9 0x0 0xc2 0xb4 0x0 0x93 0x0 0x0 0x0 0x6 0xc2 0x5d 0x0 0x93 0x0 0x0 0x0 +0xca 0x1 0xc2 0x5c 0x0 0x0 0x0 0x0 0x0 0x0 0xc3 0xc6 0x0 0xa1 0x0 0x0 0x0 +0xcb 0x6 0xc3 0x5d 0x0 0xa1 0x0 0x0 0x0 0x0 0xc4 0xd0 0x0 0xa6 0x0 0x0 0x0 +0xcc 0x6 0xc4 0x5d 0x0 0xa6 0x0 0x0 0x0 0x1 0xc4 0x5b 0x0 0x0 0x0 0x0 0x0 +0xcd 0x0 0xc5 0xe8 0x0 0xb9 0x0 0x0 0x0 0x6 0xc5 0x5d 0x0 0xb9 0x0 0x0 0x0 +0xce 0x1 0xc6 0x5a 0x0 0x0 0x0 0x0 0x0 0x0 0xc6 0xe7 0x0 0xb7 0x0 0x0 0x0 +0xcf 0x6 0xc6 0x5d 0x0 0xb7 0x0 0x0 0x0 0x1 0xc7 0x5f 0x0 0x0 0x0 0x0 0x0 +0xd0 0x0 0xc7 0xe9 0x0 0xba 0x0 0x0 0x0 0x6 0xc7 0x5d 0x0 0xba 0x0 0x0 0x0 +0xd1 0x0 0xc8 0xf5 0x0 0xc6 0x0 0x0 0x0 0x6 0xc8 0x5d 0x0 0xc6 0x0 0x0 0x0 +0xd2 0x0 0xc9 0x2 0x1 0xcf 0x0 0x0 0x0 0x6 0xc9 0x5d 0x0 0xcf 0x0 0x0 0x0 +0xd3 0x1 0xc9 0x5e 0x0 0x0 0x0 0x0 0x0 0x0 0xca 0x13 0x1 0xdd 0x0 0x0 0x0 +0xd4 0x6 0xca 0x5d 0x0 0xdd 0x0 0x0 0x0 0x1 0xcb 0x5d 0x0 0x0 0x0 0x0 0x0 +0xd5 0x0 0xcb 0x18 0x1 0xdf 0x0 0x0 0x0 0x6 0xcb 0x5d 0x0 0xdf 0x0 0x0 0x0 +0xd6 0x1 0xcc 0x56 0x0 0x0 0x0 0x0 0x0 0x0 0xcc 0x27 0x1 0xea 0x0 0x0 0x0 +0xd7 0x6 0xcc 0x5d 0x0 0xea 0x0 0x0 0x0 0x0 0xcd 0x2a 0x1 0xed 0x0 0x0 0x0 +0xd8 0x6 0xcd 0x5d 0x0 0xed 0x0 0x0 0x0 0x0 0xce 0x35 0x1 0xf3 0x0 0x0 0x0 +0xd9 0x6 0xce 0x5d 0x0 0xf3 0x0 0x0 0x0 0x1 0xce 0x55 0x0 0x0 0x0 0x0 0x0 +0xda 0x0 0xcf 0x44 0x1 0xfe 0x0 0x0 0x0 0x6 0xcf 0x5d 0x0 0xfe 0x0 0x0 0x0 +0xdb 0x1 0xcf 0x44 0x0 0x0 0x0 0x0 0x0 0x0 0xd0 0x4f 0x1 0x9 0x1 0x0 0x0 +0xdc 0x6 0xd0 0x5d 0x0 0x9 0x1 0x0 0x0 0x1 0xd1 0x4a 0x0 0x0 0x0 0x0 0x0 +0xdd 0x0 0xd1 0x37 0x1 0xf2 0x0 0x0 0x0 0x6 0xd1 0x5d 0x0 0xf2 0x0 0x0 0x0 +0xde 0x0 0xd2 0x71 0x1 0x1e 0x1 0x0 0x0 0x6 0xd2 0x5d 0x0 0x1e 0x1 0x0 0x0 +0xdf 0x0 0xd3 0x42 0x1 0xf8 0x0 0xfe 0xff 0x6 0xd3 0x5d 0x0 0xf8 0x0 0xfe 0xff +0xe0 0x1 0xd3 0x49 0x0 0x0 0x0 0x0 0x0 0x0 0xd4 0x59 0x1 0x8 0x1 0x0 0x0 +0xe1 0x6 0xd4 0x5d 0x0 0x8 0x1 0x0 0x0 0x1 0xd4 0x48 0x0 0x0 0x0 0x0 0x0 +0xe2 0x0 0xd5 0x75 0x1 0x22 0x1 0x0 0x0 0x6 0xd5 0x5d 0x0 0x22 0x1 0x0 0x0 +0xe3 0x1 0xd5 0x47 0x0 0x0 0x0 0x0 0x0 0x0 0xd6 0x64 0x1 0x10 0x1 0x0 0x0 +0xe4 0x6 0xd6 0x5d 0x0 0x10 0x1 0x0 0x0 0x0 0xd7 0x38 0x1 0xec 0x0 0xff 0xff +0xe5 0x6 0xd7 0x5d 0x0 0xec 0x0 0xff 0xff 0x1 0xd8 0x48 0x0 0x0 0x0 0x0 0x0 +0xe6 0x0 0xd8 0x66 0x1 0x14 0x1 0xff 0xff 0x6 0xd8 0x5d 0x0 0x14 0x1 0xff 0xff +0xe7 0x0 0xd9 0x58 0x1 0xc 0x1 0x0 0x0 0x6 0xd9 0x5d 0x0 0xc 0x1 0x0 0x0 +0xe8 0x1 0xd9 0x49 0x0 0x0 0x0 0x0 0x0 0x0 0xda 0x3b 0x1 0xf6 0x0 0x0 0x0 +0xe9 0x6 0xda 0x5d 0x0 0xf6 0x0 0x0 0x0 0x0 0xdb 0x53 0x1 0xa 0x1 0x0 0x0 +0xea 0x6 0xdb 0x5d 0x0 0xa 0x1 0x0 0x0 0x0 0xdc 0x38 0x1 0xf5 0x0 0x0 0x0 +0xeb 0x6 0xdc 0x5d 0x0 0xf5 0x0 0x0 0x0 0x4 0xdd 0x5a 0x0 0x15 0x0 0x0 0x0 +0xec 0x1 0xdd 0x4a 0x0 0x0 0x0 0x0 0x0 0x0 0xdd 0x7 0x1 0xd5 0x0 0x0 0x0 +0xed 0x6 0xdd 0x5d 0x0 0xd5 0x0 0x0 0x0 0x0 0xde 0x11 0x1 0xdb 0x0 0xfe 0xff +0xee 0x6 0xde 0x5d 0x0 0xdb 0x0 0xfe 0xff 0x1 0xde 0x4b 0x0 0x0 0x0 0x0 0x0 +0xef 0x0 0xdf 0x7 0x1 0xd1 0x0 0x0 0x0 0x6 0xdf 0x5d 0x0 0xd1 0x0 0x0 0x0 +0xf0 0x1 0xdf 0x4c 0x0 0x0 0x0 0x0 0x0 0x0 0xe0 0xc 0x1 0xd4 0x0 0x0 0x0 +0xf1 0x6 0xe0 0x5d 0x0 0xd4 0x0 0x0 0x0 0x0 0xe1 0xd6 0x0 0xa9 0x0 0x0 0x0 +0xf2 0x6 0xe1 0x5d 0x0 0xa9 0x0 0x0 0x0 0x1 0xe2 0x4d 0x0 0x0 0x0 0x0 0x0 +0xf3 0x0 0xe2 0xdb 0x0 0xac 0x0 0x0 0x0 0x6 0xe2 0x5d 0x0 0xac 0x0 0x0 0x0 +0xf4 0x0 0xe4 0x2d 0x1 0xad 0x0 0x0 0x0 0x6 0xe4 0x5d 0x0 0xad 0x0 0x0 0x0 +0xf5 0x1 0xe4 0x4e 0x0 0x0 0x0 0x0 0x0 0x0 0xe6 0xf3 0x0 0x8b 0x0 0x0 0x0 +0xf6 0x6 0xe6 0x5d 0x0 0x8b 0x0 0x0 0x0 0x1 0xe6 0x4f 0x0 0x0 0x0 0x0 0x0 +0xf7 0x1 0xe8 0x51 0x0 0x0 0x0 0x0 0x0 0x1 0xeb 0x52 0x0 0x0 0x0 0x0 0x0 +0xf8 0x1 0xee 0x53 0x0 0x0 0x0 0x0 0x0 0x0 0xf0 0xf7 0x0 0x87 0x0 0x0 0x0 +0xf9 0x6 0xf0 0x74 0x0 0x87 0x0 0x0 0x0 0x1 0xf0 0x54 0x0 0x0 0x0 0x0 0x0 +0xfa 0x0 0xf2 0xd8 0x0 0x7f 0x0 0x0 0x0 0x6 0xf2 0x8c 0x0 0x7f 0x0 0x0 0x0 +0xfb 0x1 0xf2 0x55 0x0 0x0 0x0 0x0 0x0 0x0 0xf4 0xcb 0x0 0x7c 0x0 0x0 0x0 +0xfc 0x6 0xf4 0x8c 0x0 0x7c 0x0 0x0 0x0 0x1 0xf5 0x56 0x0 0x0 0x0 0x0 0x0 +0xfd 0x0 0xf6 0x1c 0x1 0x75 0x0 0x0 0x0 0x6 0xf6 0xa1 0x0 0x75 0x0 0x0 0x0 +0xfe 0x1 0xf6 0x58 0x0 0x0 0x0 0x0 0x0 0x1 0xf7 0x57 0x0 0x0 0x0 0x0 0x0 +0xff 0x0 0xf8 0x67 0x1 0x3a 0x0 0x2 0x0 0x6 0xf8 0xa1 0x0 0x3a 0x0 0x2 0x0 +0x0 0x0 0xf9 0x24 0x1 0x2 0x0 0x0 0x0 0x6 0xf9 0xa1 0x0 0x2 0x0 0x0 0x0 +0x1 0x1 0xfa 0x58 0x0 0x0 0x0 0x0 0x0 0x0 0xfa 0x19 0x1 0xd6 0xff 0x0 0x0 +0x2 0x6 0xfa 0xa1 0x0 0xd6 0xff 0x0 0x0 0x0 0xfb 0x1e 0x1 0xb9 0xff 0x0 0x0 +0x3 0x6 0xfb 0xa1 0x0 0xb9 0xff 0x0 0x0 0x0 0xfc 0x22 0x1 0xa6 0xff 0x0 0x0 +0x4 0x6 0xfc 0xa1 0x0 0xa6 0xff 0x0 0x0 0x1 0xfc 0x59 0x0 0x0 0x0 0x0 0x0 +0x5 0x0 0xfd 0x68 0x1 0x47 0xff 0x0 0x0 0x6 0xfd 0xa1 0x0 0x47 0xff 0x0 0x0 +0x6 0x0 0xfe 0x3e 0x1 0x58 0xff 0x0 0x0 0x6 0xfe 0xa1 0x0 0x58 0xff 0x0 0x0 +0x7 0x0 0xff 0x32 0x1 0x5f 0xff 0x0 0x0 0x6 0xff 0xa1 0x0 0x5f 0xff 0x0 0x0 +0x8 0x0 0x0 0x19 0x1 0x67 0xff 0x0 0x0 0x6 0x0 0xa1 0x0 0x67 0xff 0x0 0x0 +0x9 0x0 0x1 0xff 0x0 0x7f 0xff 0x0 0x0 0x6 0x1 0xa1 0x0 0x7f 0xff 0x0 0x0 +0xa 0x1 0x1 0x5a 0x0 0x0 0x0 0x0 0x0 0x0 0x2 0xf5 0x0 0x92 0xff 0x0 0x0 +0xb 0x6 0x2 0xa1 0x0 0x92 0xff 0x0 0x0 0x1 0x3 0x57 0x0 0x0 0x0 0x0 0x0 +0xc 0x0 0x4 0xe1 0x0 0x9f 0xff 0xfe 0xff 0x6 0x4 0xa1 0x0 0x9f 0xff 0xfe 0xff +0xd 0x1 0x5 0x58 0x0 0x0 0x0 0x0 0x0 0x1 0xb 0x59 0x0 0x0 0x0 0x0 0x0 +0xe 0x0 0xc 0xa3 0x0 0x91 0xff 0x0 0x0 0x6 0xc 0xb8 0x0 0x91 0xff 0x0 0x0 +0xf 0x1 0xd 0x5a 0x0 0x0 0x0 0x0 0x0 0x1 0xe 0x60 0x0 0x0 0x0 0x0 0x0 +0x10 0x1 0x10 0x61 0x0 0x0 0x0 0x0 0x0 0x1 0x13 0x68 0x0 0x0 0x0 0x0 0x0 +0x11 0x1 0x14 0x6d 0x0 0x0 0x0 0x0 0x0 0x1 0x17 0x6c 0x0 0x0 0x0 0x0 0x0 +0x12 0x1 0x18 0x65 0x0 0x0 0x0 0x0 0x0 0x1 0x19 0x6a 0x0 0x0 0x0 0x0 0x0 +0x13 0x0 0x1b 0xe5 0x0 0xa0 0xff 0x0 0x0 0x6 0x1b 0x54 0x1 0xa0 0xff 0x0 0x0 +0x14 0x1 0x1c 0x6c 0x0 0x0 0x0 0x0 0x0 0x0 0x1d 0x12 0x1 0xa6 0xff 0x0 0x0 +0x15 0x6 0x1d 0x54 0x1 0xa6 0xff 0x0 0x0 0x1 0x1d 0x6d 0x0 0x0 0x0 0x0 0x0 +0x16 0x0 0x1e 0x10 0x1 0xaa 0xff 0x0 0x0 0x6 0x1e 0x6b 0x1 0xaa 0xff 0x0 0x0 +0x17 0x1 0x1e 0x69 0x0 0x0 0x0 0x0 0x0 0x0 0x1f 0x23 0x1 0xb2 0xff 0x0 0x0 +0x18 0x6 0x1f 0x6b 0x1 0xb2 0xff 0x0 0x0 0x0 0x20 0x46 0x1 0xac 0xff 0x0 0x0 +0x19 0x6 0x20 0x6b 0x1 0xac 0xff 0x0 0x0 0x1 0x21 0x6b 0x0 0x0 0x0 0x0 0x0 +0x1a 0x0 0x21 0x66 0x1 0xa7 0xff 0x0 0x0 0x6 0x21 0x4c 0x1 0xa7 0xff 0x0 0x0 +0x1b 0x1 0x22 0x6e 0x0 0x0 0x0 0x0 0x0 0x0 0x22 0x8f 0x1 0x9f 0xff 0x0 0x0 +0x1c 0x6 0x22 0x36 0x1 0x9f 0xff 0x0 0x0 0x0 0x23 0xdf 0x1 0x8b 0xff 0x0 0x0 +0x1d 0x6 0x23 0x36 0x1 0x8b 0xff 0x0 0x0 0x1 0x23 0x70 0x0 0x0 0x0 0x0 0x0 +0x1e 0x0 0x24 0x92 0x1 0x9d 0xff 0xff 0xff 0x6 0x24 0x19 0x1 0x9d 0xff 0xff 0xff +0x1f 0x0 0x25 0x82 0x1 0xa1 0xff 0x0 0x0 0x6 0x25 0x2 0x1 0xa1 0xff 0x0 0x0 +0x20 0x1 0x25 0x6a 0x0 0x0 0x0 0x0 0x0 0x0 0x26 0x2a 0x1 0xb7 0xff 0x0 0x0 +0x21 0x6 0x26 0xef 0x0 0xb7 0xff 0x0 0x0 0x1 0x27 0x67 0x0 0x0 0x0 0x0 0x0 +0x22 0x0 0x27 0x6 0x1 0xbf 0xff 0x0 0x0 0x6 0x27 0xef 0x0 0xbf 0xff 0x0 0x0 +0x23 0x1 0x28 0x6e 0x0 0x0 0x0 0x0 0x0 0x0 0x29 0x1b 0x1 0xb9 0xff 0x0 0x0 +0x24 0x6 0x29 0xd8 0x0 0xb9 0xff 0x0 0x0 0x1 0x2a 0x74 0x0 0x0 0x0 0x0 0x0 +0x25 0x1 0x2b 0x70 0x0 0x0 0x0 0x0 0x0 0x0 0x2c 0xf9 0x0 0xbc 0xff 0x0 0x0 +0x26 0x6 0x2c 0xd8 0x0 0xbc 0xff 0x0 0x0 0x1 0x2d 0x72 0x0 0x0 0x0 0x0 0x0 +0x27 0x1 0x2f 0x6b 0x0 0x0 0x0 0x0 0x0 0x0 0x30 0xa5 0x0 0x86 0xff 0x0 0x0 +0x28 0x6 0x30 0xeb 0x0 0x86 0xff 0x0 0x0 0x1 0x30 0x64 0x0 0x0 0x0 0x0 0x0 +0x29 0x1 0x32 0x63 0x0 0x0 0x0 0x0 0x0 0x1 0x34 0x5c 0x0 0x0 0x0 0x0 0x0 +0x2a 0x1 0x35 0x57 0x0 0x0 0x0 0x0 0x0 0x1 0x36 0x54 0x0 0x0 0x0 0x0 0x0 +0x2b 0x0 0x38 0x78 0x0 0x7b 0xff 0x0 0x0 0x6 0x38 0x30 0x1 0x7b 0xff 0x0 0x0 +0x2c 0x1 0x39 0x4d 0x0 0x0 0x0 0x0 0x0 0x1 0x3a 0x4c 0x0 0x0 0x0 0x0 0x0 +0x2d 0x1 0x3b 0x45 0x0 0x0 0x0 0x0 0x0 0x1 0x3e 0x47 0x0 0x0 0x0 0x0 0x0 +0x2e 0x0 0x3f 0xbe 0x0 0xbb 0xff 0x0 0x0 0x6 0x3f 0x64 0x1 0xbb 0xff 0x0 0x0 +0x2f 0x1 0x3f 0x46 0x0 0x0 0x0 0x0 0x0 0x1 0x40 0x45 0x0 0x0 0x0 0x0 0x0 +0x30 0x0 0x41 0xd4 0x0 0xc4 0xff 0x0 0x0 0x6 0x41 0x81 0x1 0xc4 0xff 0x0 0x0 +0x31 0x1 0x43 0x44 0x0 0x0 0x0 0x0 0x0 0x0 0x43 0x37 0x1 0xb0 0xff 0x0 0x0 +0x32 0x6 0x43 0x97 0x1 0xb0 0xff 0x0 0x0 0x1 0x44 0x46 0x0 0x0 0x0 0x0 0x0 +0x33 0x0 0x44 0x39 0x1 0xab 0xff 0x0 0x0 0x6 0x44 0x97 0x1 0xab 0xff 0x0 0x0 +0x34 0x0 0x45 0x53 0x1 0x9f 0xff 0x0 0x0 0x6 0x45 0x97 0x1 0x9f 0xff 0x0 0x0 +0x35 0x1 0x45 0x48 0x0 0x0 0x0 0x0 0x0 0x0 0x46 0x74 0x1 0x89 0xff 0x0 0x0 +0x36 0x6 0x46 0x7b 0x1 0x89 0xff 0x0 0x0 0x0 0x47 0x6c 0x1 0x90 0xff 0x0 0x0 +0x37 0x6 0x47 0x5c 0x1 0x90 0xff 0x0 0x0 0x1 0x48 0x47 0x0 0x0 0x0 0x0 0x0 +0x38 0x0 0x48 0x4e 0x1 0x99 0xff 0x0 0x0 0x6 0x48 0x3f 0x1 0x99 0xff 0x0 0x0 +0x39 0x0 0x49 0x6d 0x1 0x95 0xff 0x0 0x0 0x6 0x49 0x25 0x1 0x95 0xff 0x0 0x0 +0x3a 0x1 0x4a 0x48 0x0 0x0 0x0 0x0 0x0 0x1 0x4c 0x49 0x0 0x0 0x0 0x0 0x0 +0x3b 0x1 0x4e 0x4a 0x0 0x0 0x0 0x0 0x0 0x0 0x4e 0x4a 0x6 0xc3 0xfd 0x0 0x0 +0x3c 0x6 0x4e 0xdf 0x0 0xc3 0xfd 0x0 0x0 0x1 0x4f 0x4b 0x0 0x0 0x0 0x0 0x0 +0x3d 0x0 0x4f 0x62 0x4 0x58 0xfe 0x0 0x0 0x6 0x4f 0xdf 0x0 0x58 0xfe 0x0 0x0 +0x3e 0x0 0x50 0x44 0x3 0xaa 0xfe 0x0 0x0 0x6 0x50 0xdf 0x0 0xaa 0xfe 0x0 0x0 +0x3f 0x0 0x51 0x95 0x2 0xe2 0xfe 0x0 0x0 0x6 0x51 0xc1 0x0 0xe2 0xfe 0x0 0x0 +0x40 0x1 0x51 0x4c 0x0 0x0 0x0 0x0 0x0 0x0 0x52 0x24 0x2 0x8 0xff 0x0 0x0 +0x41 0x6 0x52 0xc1 0x0 0x8 0xff 0x0 0x0 0x1 0x52 0x4d 0x0 0x0 0x0 0x0 0x0 +0x42 0x0 0x53 0xcd 0x1 0x2c 0xff 0x0 0x0 0x6 0x53 0xa4 0x0 0x2c 0xff 0x0 0x0 +0x43 0x0 0x54 0x96 0x1 0x4b 0xff 0x0 0x0 0x6 0x54 0xa4 0x0 0x4b 0xff 0x0 0x0 +0x44 0x0 0x55 0x66 0x1 0x5d 0xff 0x0 0x0 0x6 0x55 0xa4 0x0 0x5d 0xff 0x0 0x0 +0x45 0x0 0x56 0x44 0x1 0x6e 0xff 0x0 0x0 0x6 0x56 0xa4 0x0 0x6e 0xff 0x0 0x0 +0x46 0x0 0x57 0x1c 0x1 0x7b 0xff 0x1 0x0 0x6 0x57 0x90 0x0 0x7b 0xff 0x1 0x0 +0x47 0x1 0x57 0x4e 0x0 0x0 0x0 0x0 0x0 0x0 0x58 0xfc 0x0 0x8a 0xff 0x0 0x0 +0x48 0x6 0x58 0x90 0x0 0x8a 0xff 0x0 0x0 0x1 0x59 0x4f 0x0 0x0 0x0 0x0 0x0 +0x49 0x0 0x59 0xe2 0x0 0x97 0xff 0x0 0x0 0x6 0x59 0x90 0x0 0x97 0xff 0x0 0x0 +0x4a 0x0 0x5a 0xd8 0x0 0x9d 0xff 0x0 0x0 0x6 0x5a 0x90 0x0 0x9d 0xff 0x0 0x0 +0x4b 0x0 0x5b 0xc6 0x0 0xa5 0xff 0x0 0x0 0x6 0x5b 0x90 0x0 0xa5 0xff 0x0 0x0 +0x4c 0x1 0x5b 0x50 0x0 0x0 0x0 0x0 0x0 0x1 0x5c 0x57 0x0 0x0 0x0 0x0 0x0 +0x4d 0x0 0x5d 0xef 0x0 0x92 0xff 0x0 0x0 0x6 0x5d 0x90 0x0 0x92 0xff 0x0 0x0 +0x4e 0x1 0x5d 0x50 0x0 0x0 0x0 0x0 0x0 0x1 0x60 0x4f 0x0 0x0 0x0 0x0 0x0 +0x4f 0x0 0x61 0xdb 0x0 0x9d 0xff 0x0 0x0 0x6 0x61 0x90 0x0 0x9d 0xff 0x0 0x0 +0x50 0x1 0x61 0x4e 0x0 0x0 0x0 0x0 0x0 0x1 0x62 0x48 0x0 0x0 0x0 0x0 0x0 +0x51 0x1 0x65 0x41 0x0 0x0 0x0 0x0 0x0 0x1 0x66 0x43 0x0 0x0 0x0 0x0 0x0 +0x52 0x1 0x67 0x44 0x0 0x0 0x0 0x0 0x0 0x1 0x6a 0x4b 0x0 0x0 0x0 0x0 0x0 +0x53 0x1 0x6b 0x4c 0x0 0x0 0x0 0x0 0x0 0x1 0x6c 0x47 0x0 0x0 0x0 0x0 0x0 +0x54 0x1 0x6e 0x4e 0x0 0x0 0x0 0x0 0x0 0x1 0x70 0x51 0x0 0x0 0x0 0x0 0x0 +0x55 0x1 0x71 0x52 0x0 0x0 0x0 0x0 0x0 0x0 0x72 0xc5 0x0 0x99 0xff 0x1 0x0 +0x56 0x6 0x72 0x13 0x1 0x99 0xff 0x1 0x0 0x0 0x74 0xec 0x0 0xc2 0xff 0x1 0x0 +0x57 0x6 0x74 0x2f 0x1 0xc2 0xff 0x1 0x0 0x1 0x74 0x50 0x0 0x0 0x0 0x0 0x0 +0x58 0x1 0x76 0x52 0x0 0x0 0x0 0x0 0x0 0x0 0x76 0x2d 0x1 0x2f 0x0 0x0 0x0 +0x59 0x6 0x76 0x44 0x1 0x2f 0x0 0x0 0x0 0x0 0x77 0xeb 0x0 0x65 0x0 0x0 0x0 +0x5a 0x6 0x77 0x44 0x1 0x65 0x0 0x0 0x0 0x0 0x78 0xe9 0x0 0x67 0x0 0x0 0x0 +0x5b 0x6 0x78 0x44 0x1 0x67 0x0 0x0 0x0 0x1 0x78 0x51 0x0 0x0 0x0 0x0 0x0 +0x5c 0x0 0x79 0xdb 0x0 0xae 0x0 0x0 0x0 0x6 0x79 0x44 0x1 0xae 0x0 0x0 0x0 +0x5d 0x1 0x79 0x50 0x0 0x0 0x0 0x0 0x0 0x0 0x7a 0xe5 0x0 0xf4 0x0 0x2 0x0 +0x5e 0x6 0x7a 0x26 0x1 0xf4 0x0 0x2 0x0 0x0 0x7b 0xe3 0x0 0xfc 0x0 0x0 0x0 +0x5f 0x6 0x7b 0x13 0x1 0xfc 0x0 0x0 0x0 0x0 0x7c 0xc2 0x0 0xd 0x1 0x0 0x0 +0x60 0x6 0x7c 0xff 0x0 0xd 0x1 0x0 0x0 0x0 0x7d 0xc2 0x0 0x4c 0x1 0x0 0x0 +0x61 0x6 0x7d 0xff 0x0 0x4c 0x1 0x0 0x0 0x1 0x7d 0x4f 0x0 0x0 0x0 0x0 0x0 +0x62 0x0 0x7e 0x96 0x0 0x43 0x1 0x0 0x0 0x6 0x7e 0xe7 0x0 0x43 0x1 0x0 0x0 +0x63 0x1 0x7e 0x52 0x0 0x0 0x0 0x0 0x0 0x0 0x7f 0x48 0x0 0x5d 0x1 0x0 0x0 +0x64 0x6 0x7f 0xd4 0x0 0x5d 0x1 0x0 0x0 0x1 0x7f 0x55 0x0 0x0 0x0 0x0 0x0 +0x65 0x0 0x80 0xe8 0xff 0x63 0x1 0x0 0x0 0x6 0x80 0xd4 0x0 0x63 0x1 0x0 0x0 +0x66 0x0 0x81 0x9a 0xff 0x7c 0x1 0x0 0x0 0x6 0x81 0xb5 0x0 0x7c 0x1 0x0 0x0 +0x67 0x1 0x82 0x56 0x0 0x0 0x0 0x0 0x0 0x0 0x82 0x69 0xff 0x60 0x1 0x0 0x0 +0x68 0x6 0x82 0xb5 0x0 0x60 0x1 0x0 0x0 0x0 0x83 0x67 0xff 0x2b 0x1 0x0 0x0 +0x69 0x6 0x83 0xb5 0x0 0x2b 0x1 0x0 0x0 0x1 0x83 0x57 0x0 0x0 0x0 0x0 0x0 +0x6a 0x0 0x84 0x5f 0xff 0x1f 0x1 0x0 0x0 0x6 0x84 0x9f 0x0 0x1f 0x1 0x0 0x0 +0x6b 0x1 0x84 0x53 0x0 0x0 0x0 0x0 0x0 0x0 0x85 0x62 0xff 0xf 0x1 0x0 0x0 +0x6c 0x6 0x85 0x9f 0x0 0xf 0x1 0x0 0x0 0x0 0x86 0x6a 0xff 0xf8 0x0 0x0 0x0 +0x6d 0x6 0x86 0x9f 0x0 0xf8 0x0 0x0 0x0 0x1 0x87 0x51 0x0 0x0 0x0 0x0 0x0 +0x6e 0x0 0x87 0xd6 0xfe 0xca 0x1 0x0 0x0 0x6 0x87 0x9f 0x0 0xca 0x1 0x0 0x0 +0x6f 0x1 0x88 0x4b 0x0 0x0 0x0 0x0 0x0 0x0 0x88 0xba 0xff 0x7d 0x0 0x0 0x0 +0x70 0x6 0x88 0x8c 0x0 0x7d 0x0 0x0 0x0 0x0 0x89 0xc1 0xff 0x96 0x0 0x0 0x0 +0x71 0x6 0x89 0x8c 0x0 0x96 0x0 0x0 0x0 0x1 0x89 0x4c 0x0 0x0 0x0 0x0 0x0 +0x72 0x0 0x8a 0xc5 0xff 0xb1 0x0 0x0 0x0 0x6 0x8a 0x8c 0x0 0xb1 0x0 0x0 0x0 +0x73 0x0 0x8b 0xcf 0xff 0xbf 0x0 0x0 0x0 0x6 0x8b 0x8c 0x0 0xbf 0x0 0x0 0x0 +0x74 0x0 0x8c 0xda 0xff 0xce 0x0 0x0 0x0 0x6 0x8c 0x8c 0x0 0xce 0x0 0x0 0x0 +0x75 0x1 0x8d 0x47 0x0 0x0 0x0 0x0 0x0 0x0 0x8d 0xd6 0xff 0xd0 0x0 0x0 0x0 +0x76 0x6 0x8d 0x8c 0x0 0xd0 0x0 0x0 0x0 0x0 0x8e 0xd8 0xff 0xd6 0x0 0x0 0x0 +0x77 0x6 0x8e 0x8c 0x0 0xd6 0x0 0x0 0x0 0x1 0x8e 0x43 0x0 0x0 0x0 0x0 0x0 +0x78 0x0 0x8f 0xe7 0xff 0xe3 0x0 0x0 0x0 0x6 0x8f 0x8c 0x0 0xe3 0x0 0x0 0x0 +0x79 0x0 0x90 0xfe 0xff 0xee 0x0 0x1 0x0 0x6 0x90 0x8c 0x0 0xee 0x0 0x1 0x0 +0x7a 0x1 0x90 0x41 0x0 0x0 0x0 0x0 0x0 0x0 0x91 0x10 0x0 0xf9 0x0 0x0 0x0 +0x7b 0x6 0x91 0x8c 0x0 0xf9 0x0 0x0 0x0 0x1 0x92 0x3f 0x0 0x0 0x0 0x0 0x0 +0x7c 0x0 0x92 0x17 0x0 0xe 0x1 0x0 0x0 0x6 0x92 0x8c 0x0 0xe 0x1 0x0 0x0 +0x7d 0x1 0x93 0x45 0x0 0x0 0x0 0x0 0x0 0x0 0x93 0x1b 0x0 0x22 0x1 0x1 0x0 +0x7e 0x6 0x93 0x8c 0x0 0x22 0x1 0x1 0x0 0x0 0x94 0x24 0x0 0x54 0x1 0x0 0x0 +0x7f 0x6 0x94 0x8c 0x0 0x54 0x1 0x0 0x0 0x0 0x95 0x1c 0x0 0x33 0x1 0x0 0x0 +0x80 0x6 0x95 0x8c 0x0 0x33 0x1 0x0 0x0 0x1 0x95 0x48 0x0 0x0 0x0 0x0 0x0 +0x81 0x0 0x96 0x29 0x0 0x49 0x1 0x0 0x0 0x6 0x96 0x8c 0x0 0x49 0x1 0x0 0x0 +0x82 0x1 0x96 0x47 0x0 0x0 0x0 0x0 0x0 0x0 0x97 0x2e 0x0 0x56 0x1 0x0 0x0 +0x83 0x6 0x97 0x8c 0x0 0x56 0x1 0x0 0x0 0x1 0x98 0x4e 0x0 0x0 0x0 0x0 0x0 +0x84 0x0 0x98 0x2e 0x0 0x5f 0x1 0x0 0x0 0x6 0x98 0x77 0x0 0x5f 0x1 0x0 0x0 +0x85 0x0 0x99 0x2a 0x0 0x86 0x1 0x0 0x0 0x6 0x99 0x77 0x0 0x86 0x1 0x0 0x0 +0x86 0x4 0x99 0xbc 0x0 0x86 0x1 0x0 0x0 0x0 0x9a 0x21 0x0 0x5c 0x1 0x0 0x0 +0x87 0x6 0x9a 0x77 0x0 0x5c 0x1 0x0 0x0 0x1 0x9a 0x4f 0x0 0x0 0x0 0x0 0x0 +0x88 0x0 0x9b 0x26 0x0 0x6e 0x1 0x0 0x0 0x6 0x9b 0x77 0x0 0x6e 0x1 0x0 0x0 +0x89 0x1 0x9b 0x50 0x0 0x0 0x0 0x0 0x0 0x0 0x9c 0x2f 0x0 0x7d 0x1 0x0 0x0 +0x8a 0x6 0x9c 0x77 0x0 0x7d 0x1 0x0 0x0 0x1 0x9d 0x52 0x0 0x0 0x0 0x0 0x0 +0x8b 0x0 0x9d 0x43 0x0 0x7f 0x1 0x0 0x0 0x6 0x9d 0x77 0x0 0x7f 0x1 0x0 0x0 +0x8c 0x0 0x9e 0x92 0x0 0x82 0x1 0x0 0x0 0x6 0x9e 0x77 0x0 0x82 0x1 0x0 0x0 +0x8d 0x1 0x9f 0x53 0x0 0x0 0x0 0x0 0x0 0x0 0x9f 0xba 0x0 0x87 0x1 0x0 0x0 +0x8e 0x6 0x9f 0x77 0x0 0x87 0x1 0x0 0x0 0x0 0xa0 0xe4 0x0 0xae 0x1 0x0 0x0 +0x8f 0x6 0xa0 0x77 0x0 0xae 0x1 0x0 0x0 0x1 0xa0 0x54 0x0 0x0 0x0 0x0 0x0 +0x90 0x0 0xa1 0xd6 0x0 0x77 0x1 0x0 0x0 0x6 0xa1 0x63 0x0 0x77 0x1 0x0 0x0 +0x91 0x1 0xa1 0x55 0x0 0x0 0x0 0x0 0x0 0x0 0xa2 0xe5 0x0 0x81 0x1 0x0 0x0 +0x92 0x6 0xa2 0x63 0x0 0x81 0x1 0x0 0x0 0x0 0xa3 0xeb 0x0 0x89 0x1 0x0 0x0 +0x93 0x6 0xa3 0x63 0x0 0x89 0x1 0x0 0x0 0x1 0xa4 0x54 0x0 0x0 0x0 0x0 0x0 +0x94 0x0 0xa4 0xf2 0x0 0x91 0x1 0x0 0x0 0x6 0xa4 0x63 0x0 0x91 0x1 0x0 0x0 +0x95 0x0 0xa5 0xef 0x0 0x98 0x1 0x1 0x0 0x6 0xa5 0x63 0x0 0x98 0x1 0x1 0x0 +0x96 0x1 0xa5 0x55 0x0 0x0 0x0 0x0 0x0 0x0 0xa6 0xef 0x0 0x99 0x1 0x0 0x0 +0x97 0x6 0xa6 0x63 0x0 0x99 0x1 0x0 0x0 0x1 0xa6 0x54 0x0 0x0 0x0 0x0 0x0 +0x98 0x0 0xa7 0xed 0x0 0x9a 0x1 0x0 0x0 0x6 0xa7 0x63 0x0 0x9a 0x1 0x0 0x0 +0x99 0x5 0xa7 0xf0 0x0 0x9a 0x1 0x0 0x0 0x0 0xa8 0xec 0x0 0x9a 0x1 0x0 0x0 +0x9a 0x6 0xa8 0x63 0x0 0x9a 0x1 0x0 0x0 0x1 0xa9 0x4d 0x0 0x0 0x0 0x0 0x0 +0x9b 0x0 0xa9 0xa 0x1 0xc2 0x1 0x0 0x0 0x6 0xa9 0x63 0x0 0xc2 0x1 0x0 0x0 +0x9c 0x1 0xaa 0x49 0x0 0x0 0x0 0x0 0x0 0x0 0xaa 0xeb 0x0 0x86 0x1 0x0 0x0 +0x9d 0x6 0xaa 0x63 0x0 0x86 0x1 0x0 0x0 0x0 0xab 0x9 0x1 0xb5 0x1 0x0 0x0 +0x9e 0x6 0xab 0x63 0x0 0xb5 0x1 0x0 0x0 0x1 0xab 0x4a 0x0 0x0 0x0 0x0 0x0 +0x9f 0x0 0xac 0xe9 0x0 0x7d 0x1 0x0 0x0 0x6 0xac 0x63 0x0 0x7d 0x1 0x0 0x0 +0xa0 0x0 0xad 0xf1 0x0 0x89 0x1 0x0 0x0 0x6 0xad 0x50 0x0 0x89 0x1 0x0 0x0 +0xa1 0x1 0xad 0x51 0x0 0x0 0x0 0x0 0x0 0x0 0xae 0xf1 0x0 0x8f 0x1 0x0 0x0 +0xa2 0x6 0xae 0x50 0x0 0x8f 0x1 0x0 0x0 0x1 0xaf 0x52 0x0 0x0 0x0 0x0 0x0 +0xa3 0x0 0xaf 0x7 0x1 0xb8 0x1 0x0 0x0 0x6 0xaf 0x50 0x0 0xb8 0x1 0x0 0x0 +0xa4 0x0 0xb0 0xf7 0x0 0xa1 0x1 0x0 0x0 0x6 0xb0 0x50 0x0 0xa1 0x1 0x0 0x0 +0xa5 0x1 0xb0 0x51 0x0 0x0 0x0 0x0 0x0 0x0 0xb1 0xd5 0x0 0x6c 0x1 0x0 0x0 +0xa6 0x6 0xb1 0x50 0x0 0x6c 0x1 0x0 0x0 0x0 0xb2 0xf4 0x0 0x9e 0x1 0x0 0x0 +0xa7 0x6 0xb2 0x50 0x0 0x9e 0x1 0x0 0x0 0x1 0xb2 0x54 0x0 0x0 0x0 0x0 0x0 +0xa8 0x0 0xb3 0xed 0x0 0x92 0x1 0x0 0x0 0x6 0xb3 0x50 0x0 0x92 0x1 0x0 0x0 +0xa9 0x1 0xb3 0x57 0x0 0x0 0x0 0x0 0x0 0x0 0xb4 0xd5 0x0 0x69 0x1 0x0 0x0 +0xaa 0x6 0xb4 0x50 0x0 0x69 0x1 0x0 0x0 0x1 0xb5 0x56 0x0 0x0 0x0 0x0 0x0 +0xab 0x0 0xb5 0xe2 0x0 0x7f 0x1 0x0 0x0 0x6 0xb5 0x50 0x0 0x7f 0x1 0x0 0x0 +0xac 0x0 0xb6 0xde 0x0 0x80 0x1 0x0 0x0 0x6 0xb6 0x50 0x0 0x80 0x1 0x0 0x0 +0xad 0x0 0xb7 0xe1 0x0 0x83 0x1 0x0 0x0 0x6 0xb7 0x50 0x0 0x83 0x1 0x0 0x0 +0xae 0x0 0xb8 0xdd 0x0 0x7b 0x1 0x0 0x0 0x6 0xb8 0x50 0x0 0x7b 0x1 0x0 0x0 +0xaf 0x1 0xb8 0x54 0x0 0x0 0x0 0x0 0x0 0x0 0xb9 0xf2 0x0 0x9c 0x1 0x0 0x0 +0xb0 0x6 0xb9 0x50 0x0 0x9c 0x1 0x0 0x0 0x1 0xba 0x56 0x0 0x0 0x0 0x0 0x0 +0xb1 0x0 0xba 0xd6 0x0 0x61 0x1 0x0 0x0 0x6 0xba 0x50 0x0 0x61 0x1 0x0 0x0 +0xb2 0x0 0xbb 0xf8 0x0 0x94 0x1 0x0 0x0 0x6 0xbb 0x50 0x0 0x94 0x1 0x0 0x0 +0xb3 0x0 0xbc 0xd7 0x0 0x60 0x1 0x0 0x0 0x6 0xbc 0x50 0x0 0x60 0x1 0x0 0x0 +0xb4 0x0 0xbd 0xfb 0x0 0x96 0x1 0x0 0x0 0x6 0xbd 0x50 0x0 0x96 0x1 0x0 0x0 +0xb5 0x0 0xbe 0xf2 0x0 0x8c 0x1 0x0 0x0 0x6 0xbe 0x50 0x0 0x8c 0x1 0x0 0x0 +0xb6 0x1 0xbe 0x58 0x0 0x0 0x0 0x0 0x0 0x0 0xbf 0xd6 0x0 0x61 0x1 0x0 0x0 +0xb7 0x6 0xbf 0x50 0x0 0x61 0x1 0x0 0x0 0x0 0xc0 0xe0 0x0 0x73 0x1 0x0 0x0 +0xb8 0x6 0xc0 0x50 0x0 0x73 0x1 0x0 0x0 0x0 0xc1 0xe1 0x0 0x78 0x1 0x0 0x0 +0xb9 0x6 0xc1 0x50 0x0 0x78 0x1 0x0 0x0 0x0 0xc2 0xe1 0x0 0x77 0x1 0x0 0x0 +0xba 0x6 0xc2 0x50 0x0 0x77 0x1 0x0 0x0 0x1 0xc2 0x5a 0x0 0x0 0x0 0x0 0x0 +0xbb 0x0 0xc3 0xe0 0x0 0x75 0x1 0x0 0x0 0x6 0xc3 0x50 0x0 0x75 0x1 0x0 0x0 +0xbc 0x1 0xc3 0x5b 0x0 0x0 0x0 0x0 0x0 0x0 0xc4 0xe3 0x0 0x7d 0x1 0x0 0x0 +0xbd 0x6 0xc4 0x50 0x0 0x7d 0x1 0x0 0x0 0x0 0xc5 0xe5 0x0 0x7f 0x1 0x0 0x0 +0xbe 0x6 0xc5 0x50 0x0 0x7f 0x1 0x0 0x0 0x1 0xc6 0x5c 0x0 0x0 0x0 0x0 0x0 +0xbf 0x0 0xc6 0xe4 0x0 0x7e 0x1 0x0 0x0 0x6 0xc6 0x50 0x0 0x7e 0x1 0x0 0x0 +0xc0 0x0 0xc7 0xe5 0x0 0x7f 0x1 0x0 0x0 0x6 0xc7 0x50 0x0 0x7f 0x1 0x0 0x0 +0xc1 0x1 0xc7 0x5d 0x0 0x0 0x0 0x0 0x0 0x0 0xc8 0xfb 0x0 0xa6 0x1 0x0 0x0 +0xc2 0x6 0xc8 0x50 0x0 0xa6 0x1 0x0 0x0 0x1 0xc8 0x5e 0x0 0x0 0x0 0x0 0x0 +0xc3 0x0 0xc9 0xd0 0x0 0x6e 0x1 0x0 0x0 0x6 0xc9 0x50 0x0 0x6e 0x1 0x0 0x0 +0xc4 0x0 0xca 0xdc 0x0 0x7a 0x1 0x0 0x0 0x6 0xca 0x50 0x0 0x7a 0x1 0x0 0x0 +0xc5 0x1 0xcb 0x5f 0x0 0x0 0x0 0x0 0x0 0x0 0xcb 0xe0 0x0 0x7d 0x1 0x0 0x0 +0xc6 0x6 0xcb 0x50 0x0 0x7d 0x1 0x0 0x0 0x1 0xcc 0x60 0x0 0x0 0x0 0x0 0x0 +0xc7 0x0 0xcc 0xe2 0x0 0x7f 0x1 0x0 0x0 0x6 0xcc 0x50 0x0 0x7f 0x1 0x0 0x0 +0xc8 0x0 0xcd 0xe4 0x0 0x80 0x1 0x0 0x0 0x6 0xcd 0x50 0x0 0x80 0x1 0x0 0x0 +0xc9 0x1 0xcd 0x61 0x0 0x0 0x0 0x0 0x0 0x0 0xce 0xe4 0x0 0x81 0x1 0x0 0x0 +0xca 0x6 0xce 0x50 0x0 0x81 0x1 0x0 0x0 0x0 0xcf 0xe2 0x0 0x7e 0x1 0x0 0x0 +0xcb 0x6 0xcf 0x50 0x0 0x7e 0x1 0x0 0x0 0x1 0xd0 0x60 0x0 0x0 0x0 0x0 0x0 +0xcc 0x0 0xd0 0xf6 0x0 0x9e 0x1 0x0 0x0 0x6 0xd0 0x50 0x0 0x9e 0x1 0x0 0x0 +0xcd 0x0 0xd1 0xd1 0x0 0x60 0x1 0x0 0x0 0x6 0xd1 0x50 0x0 0x60 0x1 0x0 0x0 +0xce 0x0 0xd2 0xd2 0x0 0x61 0x1 0x0 0x0 0x6 0xd2 0x50 0x0 0x61 0x1 0x0 0x0 +0xcf 0x1 0xd2 0x5f 0x0 0x0 0x0 0x0 0x0 0x0 0xd3 0xd5 0x0 0x67 0x1 0x0 0x0 +0xd0 0x6 0xd3 0x50 0x0 0x67 0x1 0x0 0x0 0x0 0xd4 0xec 0x0 0x8d 0x1 0x0 0x0 +0xd1 0x6 0xd4 0x50 0x0 0x8d 0x1 0x0 0x0 0x1 0xd4 0x5e 0x0 0x0 0x0 0x0 0x0 +0xd2 0x0 0xd5 0xc9 0x0 0x53 0x1 0x0 0x0 0x6 0xd5 0x50 0x0 0x53 0x1 0x0 0x0 +0xd3 0x1 0xd6 0x5c 0x0 0x0 0x0 0x0 0x0 0x0 0xd6 0xcd 0x0 0x58 0x1 0x0 0x0 +0xd4 0x6 0xd6 0x50 0x0 0x58 0x1 0x0 0x0 0x1 0xd7 0x5b 0x0 0x0 0x0 0x0 0x0 +0xd5 0x0 0xd7 0xca 0x0 0x54 0x1 0x0 0x0 0x6 0xd7 0x50 0x0 0x54 0x1 0x0 0x0 +0xd6 0x0 0xd8 0xd2 0x0 0x5e 0x1 0x0 0x0 0x6 0xd8 0x50 0x0 0x5e 0x1 0x0 0x0 +0xd7 0x0 0xd9 0x2c 0x1 0x15 0x1 0x0 0x0 0x6 0xd9 0x50 0x0 0x15 0x1 0x0 0x0 +0xd8 0x0 0xda 0x60 0x1 0x1c 0x1 0x0 0x0 0x6 0xda 0x50 0x0 0x1c 0x1 0x0 0x0 +0xd9 0x1 0xda 0x55 0x0 0x0 0x0 0x0 0x0 0x0 0xdb 0x5d 0x1 0xd6 0x0 0x0 0x0 +0xda 0x6 0xdb 0x50 0x0 0xd6 0x0 0x0 0x0 0x4 0xdc 0x43 0x0 0xd6 0x0 0x0 0x0 +0xdb 0x1 0xdc 0x58 0x0 0x0 0x0 0x0 0x0 0x0 0xdc 0x9c 0x1 0xc8 0x0 0x0 0x0 +0xdc 0x6 0xdc 0x50 0x0 0xc8 0x0 0x0 0x0 0x0 0xdd 0x70 0x1 0x8e 0x0 0x0 0x0 +0xdd 0x6 0xdd 0x50 0x0 0x8e 0x0 0x0 0x0 0x0 0xde 0x83 0x1 0x86 0x0 0x0 0x0 +0xde 0x6 0xde 0x50 0x0 0x86 0x0 0x0 0x0 0x0 0xdf 0x93 0x1 0x86 0x0 0x0 0x0 +0xdf 0x6 0xdf 0x50 0x0 0x86 0x0 0x0 0x0 0x1 0xdf 0x57 0x0 0x0 0x0 0x0 0x0 +0xe0 0x0 0xe0 0x74 0x1 0x7d 0x0 0x0 0x0 0x6 0xe0 0x50 0x0 0x7d 0x0 0x0 0x0 +0xe1 0x0 0xe1 0x2d 0x1 0x7c 0x0 0x0 0x0 0x6 0xe1 0x50 0x0 0x7c 0x0 0x0 0x0 +0xe2 0x0 0xe3 0xf0 0x0 0xa5 0x0 0xff 0xff 0x6 0xe3 0x50 0x0 0xa5 0x0 0xff 0xff +0xe3 0x1 0xe4 0x58 0x0 0x0 0x0 0x0 0x0 0x0 0xe4 0x73 0x0 0x99 0x0 0x0 0x0 +0xe4 0x6 0xe4 0x50 0x0 0x99 0x0 0x0 0x0 0x0 0xe6 0x14 0x0 0xb5 0x0 0x0 0x0 +0xe5 0x6 0xe6 0x50 0x0 0xb5 0x0 0x0 0x0 0x0 0xe8 0xfb 0xff 0xbf 0x0 0x0 0x0 +0xe6 0x6 0xe8 0x50 0x0 0xbf 0x0 0x0 0x0 0x1 0xe9 0x59 0x0 0x0 0x0 0x0 0x0 +0xe7 0x0 0xea 0x8 0x0 0xd5 0x0 0x0 0x0 0x6 0xea 0x50 0x0 0xd5 0x0 0x0 0x0 +0xe8 0x1 0xec 0x58 0x0 0x0 0x0 0x0 0x0 0x0 0xee 0x68 0x0 0xae 0x0 0x0 0x0 +0xe9 0x6 0xee 0x64 0x0 0xae 0x0 0x0 0x0 0x0 0xf0 0xc5 0x0 0x5e 0x0 0x0 0x0 +0xea 0x6 0xf0 0x64 0x0 0x5e 0x0 0x0 0x0 0x1 0xf1 0x59 0x0 0x0 0x0 0x0 0x0 +0xeb 0x0 0xf2 0x1b 0x1 0x2f 0x0 0x1 0x0 0x6 0xf2 0x64 0x0 0x2f 0x0 0x1 0x0 +0xec 0x1 0xf2 0x58 0x0 0x0 0x0 0x0 0x0 0x0 0xf3 0x15 0x1 0x15 0x0 0x0 0x0 +0xed 0x6 0xf3 0x64 0x0 0x15 0x0 0x0 0x0 0x0 0xf4 0x2a 0x1 0x8 0x0 0x0 0x0 +0xee 0x6 0xf4 0x76 0x0 0x8 0x0 0x0 0x0 0x1 0xf5 0x59 0x0 0x0 0x0 0x0 0x0 +0xef 0x0 0xf5 0x6c 0x1 0x13 0x0 0x0 0x0 0x6 0xf5 0x76 0x0 0x13 0x0 0x0 0x0 +0xf0 0x0 0xf6 0x63 0x1 0xa 0x0 0x0 0x0 0x6 0xf6 0x76 0x0 0xa 0x0 0x0 0x0 +0xf1 0x0 0xf7 0x8d 0x1 0x2 0x0 0x0 0x0 0x6 0xf7 0x76 0x0 0x2 0x0 0x0 0x0 +0xf2 0x1 0xf7 0x5a 0x0 0x0 0x0 0x0 0x0 0x0 0xf8 0xa5 0x1 0xf7 0xff 0x0 0x0 +0xf3 0x6 0xf8 0x76 0x0 0xf7 0xff 0x0 0x0 0x0 0xf9 0x22 0x2 0xd6 0xff 0x0 0x0 +0xf4 0x6 0xf9 0x76 0x0 0xd6 0xff 0x0 0x0 0x1 0xfa 0x57 0x0 0x0 0x0 0x0 0x0 +0xf5 0x0 0xfa 0x1 0x2 0xdf 0xff 0x0 0x0 0x6 0xfa 0x76 0x0 0xdf 0xff 0x0 0x0 +0xf6 0x1 0xfb 0x54 0x0 0x0 0x0 0x0 0x0 0x0 0xfb 0xd2 0x1 0xe8 0xff 0x0 0x0 +0xf7 0x6 0xfb 0x76 0x0 0xe8 0xff 0x0 0x0 0x0 0xfc 0xaf 0x1 0xf4 0xff 0x0 0x0 +0xf8 0x6 0xfc 0x76 0x0 0xf4 0xff 0x0 0x0 0x1 0xfc 0x57 0x0 0x0 0x0 0x0 0x0 +0xf9 0x0 0xfd 0x93 0x1 0x4 0x0 0x0 0x0 0x6 0xfd 0x76 0x0 0x4 0x0 0x0 0x0 +0xfa 0x0 0xfe 0x8a 0x1 0x7 0x0 0x0 0x0 0x6 0xfe 0x76 0x0 0x7 0x0 0x0 0x0 +0xfb 0x0 0xff 0x6f 0x1 0x7 0x0 0x0 0x0 0x6 0xff 0x76 0x0 0x7 0x0 0x0 0x0 +0xfc 0x1 0x0 0x58 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x5a 0x1 0x5 0x0 0x0 0x0 +0xfd 0x6 0x0 0x76 0x0 0x5 0x0 0x0 0x0 0x1 0x1 0x57 0x0 0x0 0x0 0x0 0x0 +0xfe 0x0 0x1 0x22 0x1 0x1 0x0 0x0 0x0 0x6 0x1 0x76 0x0 0x1 0x0 0x0 0x0 +0xff 0x0 0x3 0x20 0x1 0x5 0x0 0x0 0x0 0x6 0x3 0x76 0x0 0x5 0x0 0x0 0x0 +0x0 0x1 0x3 0x58 0x0 0x0 0x0 0x0 0x0 0x1 0x4 0x59 0x0 0x0 0x0 0x0 0x0 +0x1 0x0 0x8 0xfe 0x0 0x1b 0x0 0x0 0x0 0x6 0x8 0x76 0x0 0x1b 0x0 0x0 0x0 +0x2 0x1 0x9 0x59 0x0 0x0 0x0 0x0 0x0 0x1 0xe 0x58 0x0 0x0 0x0 0x0 0x0 +0x3 0x0 0xf 0xc0 0x0 0x53 0x0 0x0 0x0 0x6 0xf 0xa5 0x0 0x53 0x0 0x0 0x0 +0x4 0x0 0x11 0x5d 0x0 0x7f 0x0 0x0 0x0 0x6 0x11 0xa5 0x0 0x7f 0x0 0x0 0x0 +0x5 0x1 0x15 0x58 0x0 0x0 0x0 0x0 0x0 0x0 0x16 0x16 0x0 0xb7 0x0 0x0 0x0 +0x6 0x6 0x16 0xb9 0x0 0xb7 0x0 0x0 0x0 0x1 0x16 0x59 0x0 0x0 0x0 0x0 0x0 +0x7 0x0 0x16 0x1 0x0 0xa4 0x0 0x0 0x0 0x6 0x16 0xb9 0x0 0xa4 0x0 0x0 0x0 +0x8 0x0 0x17 0x14 0x0 0xe6 0x0 0x0 0x0 0x6 0x17 0xb9 0x0 0xe6 0x0 0x0 0x0 +0x9 0x1 0x17 0x58 0x0 0x0 0x0 0x0 0x0 0x0 0x18 0x14 0x0 0xff 0x0 0x0 0x0 +0xa 0x6 0x18 0xb9 0x0 0xff 0x0 0x0 0x0 0x1 0x19 0x57 0x0 0x0 0x0 0x0 0x0 +0xb 0x0 0x19 0xd 0x0 0xe3 0x0 0x0 0x0 0x6 0x19 0xb9 0x0 0xe3 0x0 0x0 0x0 +0xc 0x0 0x1a 0xe 0x0 0xef 0x0 0xff 0xff 0x6 0x1a 0xb9 0x0 0xef 0x0 0xff 0xff +0xd 0x0 0x1b 0x13 0x0 0xe5 0x0 0x0 0x0 0x6 0x1b 0xb9 0x0 0xe5 0x0 0x0 0x0 +0xe 0x0 0x1c 0xc 0x0 0xca 0x0 0x0 0x0 0x6 0x1c 0xb9 0x0 0xca 0x0 0x0 0x0 +0xf 0x0 0x1d 0xff 0xff 0xc4 0x0 0x0 0x0 0x6 0x1d 0xb9 0x0 0xc4 0x0 0x0 0x0 +0x10 0x1 0x1d 0x56 0x0 0x0 0x0 0x0 0x0 0x0 0x1e 0x9 0x0 0xe2 0x0 0x0 0x0 +0x11 0x6 0x1e 0xb9 0x0 0xe2 0x0 0x0 0x0 0x1 0x1f 0x54 0x0 0x0 0x0 0x0 0x0 +0x12 0x0 0x1f 0xc 0x0 0xd7 0x0 0x0 0x0 0x6 0x1f 0xa6 0x0 0xd7 0x0 0x0 0x0 +0x13 0x0 0x20 0x11 0x0 0xd2 0x0 0x0 0x0 0x6 0x20 0xa6 0x0 0xd2 0x0 0x0 0x0 +0x14 0x0 0x21 0x12 0x0 0xb7 0x0 0x0 0x0 0x6 0x21 0xa6 0x0 0xb7 0x0 0x0 0x0 +0x15 0x0 0x22 0xc 0x0 0xc2 0x0 0x0 0x0 0x6 0x22 0xa6 0x0 0xc2 0x0 0x0 0x0 +0x16 0x1 0x22 0x53 0x0 0x0 0x0 0x0 0x0 0x0 0x23 0x6 0x0 0xc5 0x0 0x0 0x0 +0x17 0x6 0x23 0xa6 0x0 0xc5 0x0 0x0 0x0 0x1 0x23 0x54 0x0 0x0 0x0 0x0 0x0 +0x18 0x0 0x24 0xff 0xff 0xc9 0x0 0x0 0x0 0x6 0x24 0xa6 0x0 0xc9 0x0 0x0 0x0 +0x19 0x1 0x25 0x53 0x0 0x0 0x0 0x0 0x0 0x0 0x25 0xfe 0xff 0xdf 0x0 0x0 0x0 +0x1a 0x6 0x25 0xa6 0x0 0xdf 0x0 0x0 0x0 0x0 0x26 0x4 0x0 0xc2 0x0 0x0 0x0 +0x1b 0x6 0x26 0xa6 0x0 0xc2 0x0 0x0 0x0 0x0 0x27 0x8 0x0 0xc2 0x0 0x0 0x0 +0x1c 0x6 0x27 0xa6 0x0 0xc2 0x0 0x0 0x0 0x1 0x27 0x52 0x0 0x0 0x0 0x0 0x0 +0x1d 0x0 0x28 0xe 0x0 0xd7 0x0 0x0 0x0 0x6 0x28 0xa6 0x0 0xd7 0x0 0x0 0x0 +0x1e 0x0 0x29 0x10 0x0 0xbb 0x0 0x0 0x0 0x6 0x29 0xa6 0x0 0xbb 0x0 0x0 0x0 +0x1f 0x1 0x29 0x53 0x0 0x0 0x0 0x0 0x0 0x0 0x2a 0x11 0x0 0xc4 0x0 0x0 0x0 +0x20 0x6 0x2a 0xa6 0x0 0xc4 0x0 0x0 0x0 0x0 0x2b 0x13 0x0 0xd5 0x0 0x0 0x0 +0x21 0x6 0x2b 0xa6 0x0 0xd5 0x0 0x0 0x0 0x1 0x2c 0x54 0x0 0x0 0x0 0x0 0x0 +0x22 0x0 0x2c 0xe 0x0 0xb6 0x0 0x0 0x0 0x6 0x2c 0xa6 0x0 0xb6 0x0 0x0 0x0 +0x23 0x0 0x2d 0x9 0x0 0xbc 0x0 0x0 0x0 0x6 0x2d 0xa6 0x0 0xbc 0x0 0x0 0x0 +0x24 0x0 0x2e 0x5 0x0 0xbb 0x0 0x0 0x0 0x6 0x2e 0xa6 0x0 0xbb 0x0 0x0 0x0 +0x25 0x0 0x2f 0x3 0x0 0xd5 0x0 0x0 0x0 0x6 0x2f 0xa6 0x0 0xd5 0x0 0x0 0x0 +0x26 0x0 0x30 0xfd 0xff 0xd6 0x0 0x0 0x0 0x6 0x30 0xa6 0x0 0xd6 0x0 0x0 0x0 +0x27 0x1 0x31 0x55 0x0 0x0 0x0 0x0 0x0 0x0 0x31 0xfc 0xff 0xbe 0x0 0x0 0x0 +0x28 0x6 0x31 0x94 0x0 0xbe 0x0 0x0 0x0 0x0 0x32 0xfd 0xff 0xcd 0x0 0x0 0x0 +0x29 0x6 0x32 0x94 0x0 0xcd 0x0 0x0 0x0 0x1 0x32 0x56 0x0 0x0 0x0 0x0 0x0 +0x2a 0x0 0x33 0xfc 0xff 0xd7 0x0 0x0 0x0 0x6 0x33 0x94 0x0 0xd7 0x0 0x0 0x0 +0x2b 0x0 0x34 0xfb 0xff 0xc8 0x0 0xff 0xff 0x6 0x34 0x94 0x0 0xc8 0x0 0xff 0xff +0x2c 0x0 0x35 0xff 0xff 0xb4 0x0 0x0 0x0 0x6 0x35 0x94 0x0 0xb4 0x0 0x0 0x0 +0x2d 0x1 0x36 0x57 0x0 0x0 0x0 0x0 0x0 0x1 0x37 0x58 0x0 0x0 0x0 0x0 0x0 +0x2e 0x0 0x37 0xf8 0xff 0xb6 0x0 0x0 0x0 0x6 0x37 0x94 0x0 0xb6 0x0 0x0 0x0 +0x2f 0x1 0x38 0x59 0x0 0x0 0x0 0x0 0x0 0x0 0x39 0xf9 0xff 0xa1 0x0 0x0 0x0 +0x30 0x6 0x39 0x94 0x0 0xa1 0x0 0x0 0x0 0x1 0x3a 0x5a 0x0 0x0 0x0 0x0 0x0 +0x31 0x1 0x3c 0x5c 0x0 0x0 0x0 0x0 0x0 0x1 0x3d 0x5b 0x0 0x0 0x0 0x0 0x0 +0x32 0x0 0x3d 0xd 0x0 0x94 0x0 0x0 0x0 0x6 0x3d 0x94 0x0 0x94 0x0 0x0 0x0 +0x33 0x1 0x40 0x62 0x0 0x0 0x0 0x0 0x0 0x1 0x42 0x61 0x0 0x0 0x0 0x0 0x0 +0x34 0x1 0x44 0x68 0x0 0x0 0x0 0x0 0x0 0x0 0x45 0xbf 0xff 0x84 0x0 0x0 0x0 +0x35 0x6 0x45 0xc0 0x0 0x84 0x0 0x0 0x0 0x1 0x45 0x6e 0x0 0x0 0x0 0x0 0x0 +0x36 0x1 0x47 0x66 0x0 0x0 0x0 0x0 0x0 0x0 0x47 0x74 0xff 0x56 0x0 0x0 0x0 +0x37 0x6 0x47 0xdb 0x0 0x56 0x0 0x0 0x0 0x0 0x49 0xd7 0xfe 0x3a 0x0 0x0 0x0 +0x38 0x6 0x49 0xdb 0x0 0x3a 0x0 0x0 0x0 0x1 0x49 0x68 0x0 0x0 0x0 0x0 0x0 +0x39 0x0 0x4a 0xa 0xff 0x60 0x0 0x0 0x0 0x6 0x4a 0xf1 0x0 0x60 0x0 0x0 0x0 +0x3a 0x1 0x4b 0x61 0x0 0x0 0x0 0x0 0x0 0x0 0x4c 0xdd 0xfe 0x3f 0x0 0x0 0x0 +0x3b 0x6 0x4c 0xf1 0x0 0x3f 0x0 0x0 0x0 0x1 0x4e 0x60 0x0 0x0 0x0 0x0 0x0 +0x3c 0x0 0x4e 0xd6 0xfe 0x3d 0x0 0x0 0x0 0x6 0x4e 0xf1 0x0 0x3d 0x0 0x0 0x0 +0x3d 0x1 0x4f 0x59 0x0 0x0 0x0 0x0 0x0 0x0 0x50 0xf6 0xfe 0x24 0x0 0x0 0x0 +0x3e 0x6 0x50 0xf1 0x0 0x24 0x0 0x0 0x0 0x1 0x50 0x54 0x0 0x0 0x0 0x0 0x0 +0x3f 0x0 0x52 0x8 0xff 0x26 0x0 0x0 0x0 0x6 0x52 0xf1 0x0 0x26 0x0 0x0 0x0 +0x40 0x1 0x53 0x51 0x0 0x0 0x0 0x0 0x0 0x1 0x54 0x4e 0x0 0x0 0x0 0x0 0x0 +0x41 0x0 0x55 0x7 0xff 0x40 0x0 0x0 0x0 0x6 0x55 0xf1 0x0 0x40 0x0 0x0 0x0 +0x42 0x1 0x55 0x4f 0x0 0x0 0x0 0x0 0x0 0x0 0x57 0xe 0xff 0x20 0x0 0x0 0x0 +0x43 0x6 0x57 0x6 0x1 0x20 0x0 0x0 0x0 0x1 0x58 0x52 0x0 0x0 0x0 0x0 0x0 +0x44 0x0 0x59 0xb5 0xfe 0x12 0x0 0x0 0x0 0x6 0x59 0x6 0x1 0x12 0x0 0x0 0x0 +0x45 0x0 0x5a 0xc4 0xfe 0xb 0x0 0x0 0x0 0x6 0x5a 0x6 0x1 0xb 0x0 0x0 0x0 +0x46 0x1 0x5a 0x53 0x0 0x0 0x0 0x0 0x0 0x0 0x5b 0xbb 0xfe 0xb 0x0 0x2 0x0 +0x47 0x6 0x5b 0x6 0x1 0xb 0x0 0x2 0x0 0x0 0x5c 0xac 0xfe 0xd 0x0 0x0 0x0 +0x48 0x6 0x5c 0x6 0x1 0xd 0x0 0x0 0x0 0x1 0x5c 0x54 0x0 0x0 0x0 0x0 0x0 +0x49 0x0 0x5d 0x2c 0xfe 0x1a 0x0 0x0 0x0 0x6 0x5d 0x6 0x1 0x1a 0x0 0x0 0x0 +0x4a 0x1 0x5e 0x56 0x0 0x0 0x0 0x0 0x0 0x0 0x5e 0x44 0xfe 0x22 0x0 0x0 0x0 +0x4b 0x6 0x5e 0xf2 0x0 0x22 0x0 0x0 0x0 0x1 0x5f 0x55 0x0 0x0 0x0 0x0 0x0 +0x4c 0x0 0x5f 0x76 0xfe 0x27 0x0 0x0 0x0 0x6 0x5f 0xf2 0x0 0x27 0x0 0x0 0x0 +0x4d 0x0 0x60 0xa2 0xfe 0x26 0x0 0x0 0x0 0x6 0x60 0xf2 0x0 0x26 0x0 0x0 0x0 +0x4e 0x0 0x61 0xd2 0xfe 0x1d 0x0 0x0 0x0 0x6 0x61 0xd9 0x0 0x1d 0x0 0x0 0x0 +0x4f 0x1 0x62 0x54 0x0 0x0 0x0 0x0 0x0 0x0 0x63 0xc6 0xfe 0x19 0x0 0x0 0x0 +0x50 0x6 0x63 0xd9 0x0 0x19 0x0 0x0 0x0 0x1 0x64 0x55 0x0 0x0 0x0 0x0 0x0 +0x51 0x1 0x66 0x56 0x0 0x0 0x0 0x0 0x0 0x1 0x67 0x57 0x0 0x0 0x0 0x0 0x0 +0x52 0x1 0x68 0x58 0x0 0x0 0x0 0x0 0x0 0x1 0x6b 0x59 0x0 0x0 0x0 0x0 0x0 +0x53 0x0 0x6b 0x2c 0xff 0x4a 0x0 0x0 0x0 0x6 0x6b 0xf3 0x0 0x4a 0x0 0x0 0x0 +0x54 0x1 0x6d 0x5b 0x0 0x0 0x0 0x0 0x0 0x1 0x6e 0x5a 0x0 0x0 0x0 0x0 0x0 +0x55 0x1 0x73 0x58 0x0 0x0 0x0 0x0 0x0 0x0 0x74 0xe5 0xfe 0x37 0x0 0x0 0x0 +0x56 0x6 0x74 0x3a 0x1 0x37 0x0 0x0 0x0 0x1 0x75 0x59 0x0 0x0 0x0 0x0 0x0 +0x57 0x1 0x76 0x58 0x0 0x0 0x0 0x0 0x0 0x0 0x77 0xee 0xfe 0x5 0x0 0x0 0x0 +0x58 0x6 0x77 0x56 0x1 0x5 0x0 0x0 0x0 0x1 0x78 0x59 0x0 0x0 0x0 0x0 0x0 +0x59 0x1 0x7a 0x58 0x0 0x0 0x0 0x0 0x0 0x1 0x7c 0x59 0x0 0x0 0x0 0x0 0x0 +0x5a 0x1 0x80 0x5a 0x0 0x0 0x0 0x0 0x0 0x1 0x81 0x59 0x0 0x0 0x0 0x0 0x0 +0x5b 0x1 0x84 0x5a 0x0 0x0 0x0 0x0 0x0 0x0 0x84 0xf 0x0 0xf8 0xff 0x0 0x0 +0x5c 0x6 0x84 0xfc 0x1 0xf8 0xff 0x0 0x0 0x1 0x85 0x5b 0x0 0x0 0x0 0x0 0x0 +0x5d 0x1 0x86 0x5a 0x0 0x0 0x0 0x0 0x0 0x0 0x86 0x9e 0x0 0xeb 0xff 0x0 0x0 +0x5e 0x6 0x86 0x13 0x2 0xeb 0xff 0x0 0x0 0x1 0x88 0x5b 0x0 0x0 0x0 0x0 0x0 +0x5f 0x0 0x89 0xbf 0x0 0xf0 0xff 0x0 0x0 0x6 0x89 0x13 0x2 0xf0 0xff 0x0 0x0 +0x60 0x1 0x8a 0x5a 0x0 0x0 0x0 0x0 0x0 0x1 0x8b 0x5b 0x0 0x0 0x0 0x0 0x0 +0x61 0x1 0x8d 0x5c 0x0 0x0 0x0 0x0 0x0 0x1 0x8e 0x5b 0x0 0x0 0x0 0x0 0x0 +0x62 0x0 0x91 0xea 0xff 0xf6 0xff 0x0 0x0 0x6 0x91 0x57 0x2 0xf6 0xff 0x0 0x0 +0x63 0x1 0x92 0x5c 0x0 0x0 0x0 0x0 0x0 0x1 0x93 0x5d 0x0 0x0 0x0 0x0 0x0 +0x64 0x1 0x98 0x5e 0x0 0x0 0x0 0x0 0x0 0x1 0x99 0x5f 0x0 0x0 0x0 0x0 0x0 +0x65 0x1 0x9c 0x5e 0x0 0x0 0x0 0x0 0x0 0x1 0x9d 0x5f 0x0 0x0 0x0 0x0 0x0 +0x66 0x1 0xa1 0x60 0x0 0x0 0x0 0x0 0x0 0x1 0xa2 0x61 0x0 0x0 0x0 0x0 0x0 +0x67 0x1 0xa3 0x63 0x0 0x0 0x0 0x0 0x0 0x1 0xa5 0x64 0x0 0x0 0x0 0x0 0x0 +0x68 0x1 0xaa 0x63 0x0 0x0 0x0 0x0 0x0 0x1 0xad 0x61 0x0 0x0 0x0 0x0 0x0 +0x69 0x1 0xaf 0x5f 0x0 0x0 0x0 0x0 0x0 0x1 0xb0 0x5d 0x0 0x0 0x0 0x0 0x0 +0x6a 0x1 0xb1 0x5c 0x0 0x0 0x0 0x0 0x0 0x0 0xb3 0x85 0xff 0xfb 0xff 0x0 0x0 +0x6b 0x6 0xb3 0xa8 0x6 0xfb 0xff 0x0 0x0 0x1 0xb4 0x5b 0x0 0x0 0x0 0x0 0x0 +0x6c 0x1 0xb6 0x59 0x0 0x0 0x0 0x0 0x0 0x1 0xb9 0x58 0x0 0x0 0x0 0x0 0x0 +0x6d 0x1 0xba 0x57 0x0 0x0 0x0 0x0 0x0 0x1 0xbe 0x58 0x0 0x0 0x0 0x0 0x0 +0x6e 0x1 0xbf 0x59 0x0 0x0 0x0 0x0 0x0 0x1 0xc0 0x5a 0x0 0x0 0x0 0x0 0x0 +0x6f 0x1 0xc3 0x5b 0x0 0x0 0x0 0x0 0x0 0x0 0xc3 0x30 0x0 0xec 0xff 0x0 0x0 +0x70 0x6 0xc3 0x0 0x0 0xec 0xff 0x0 0x0 0x1 0xc4 0x5d 0x0 0x0 0x0 0x0 0x0 +0x71 0x1 0xc7 0x5e 0x0 0x0 0x0 0x0 0x0 0x0 0xc8 0x8b 0x0 0xda 0xff 0x0 0x0 +0x72 0x6 0xc8 0x0 0x0 0xda 0xff 0x0 0x0 0x1 0xc9 0x5d 0x0 0x0 0x0 0x0 0x0 +0x73 0x1 0xcc 0x5e 0x0 0x0 0x0 0x0 0x0 0x0 0xcc 0xc0 0x0 0xeb 0xff 0x0 0x0 +0x74 0x6 0xcc 0xf0 0x6 0xeb 0xff 0x0 0x0 0x1 0xcf 0x5d 0x0 0x0 0x0 0x0 0x0 +0x75 0x0 0xd0 0xd8 0x0 0xeb 0xff 0x0 0x0 0x6 0xd0 0xc4 0x5 0xeb 0xff 0x0 0x0 +0x76 0x1 0xd1 0x5c 0x0 0x0 0x0 0x0 0x0 0x1 0xd4 0x5d 0x0 0x0 0x0 0x0 0x0 +0x77 0x0 0xd4 0xe4 0x0 0xfd 0xff 0x0 0x0 0x6 0xd4 0xc 0x5 0xfd 0xff 0x0 0x0 +0x78 0x1 0xd6 0x5e 0x0 0x0 0x0 0x0 0x0 0x1 0xd7 0x5f 0x0 0x0 0x0 0x0 0x0 +0x79 0x0 0xd7 0xed 0x0 0xf7 0xff 0x0 0x0 0x6 0xd7 0x82 0x4 0xf7 0xff 0x0 0x0 +0x7a 0x1 0xd8 0x60 0x0 0x0 0x0 0x0 0x0 0x0 0xda 0xec 0x0 0xef 0xff 0x0 0x0 +0x7b 0x6 0xda 0x1b 0x4 0xef 0xff 0x0 0x0 0x1 0xdb 0x62 0x0 0x0 0x0 0x0 0x0 +0x7c 0x1 0xdc 0x63 0x0 0x0 0x0 0x0 0x0 0x1 0xdd 0x64 0x0 0x0 0x0 0x0 0x0 +0x7d 0x0 0xdd 0xeb 0x0 0xef 0xff 0x0 0x0 0x6 0xdd 0xa5 0x3 0xef 0xff 0x0 0x0 +0x7e 0x1 0xe1 0x65 0x0 0x0 0x0 0x0 0x0 0x0 0xe1 0xff 0x0 0xef 0xff 0x0 0x0 +0x7f 0x6 0xe1 0x52 0x3 0xef 0xff 0x0 0x0 0x1 0xe2 0x64 0x0 0x0 0x0 0x0 0x0 +0x80 0x0 0xe5 0xe6 0x0 0xe3 0xff 0x0 0x0 0x6 0xe5 0x27 0x3 0xe3 0xff 0x0 0x0 +0x81 0x1 0xe6 0x63 0x0 0x0 0x0 0x0 0x0 0x0 0xe9 0xfa 0x0 0xcc 0xff 0x0 0x0 +0x82 0x6 0xe9 0x27 0x3 0xcc 0xff 0x0 0x0 0x1 0xeb 0x62 0x0 0x0 0x0 0x0 0x0 +0x83 0x0 0xed 0xee 0x0 0xfa 0xff 0x0 0x0 0x6 0xed 0x14 0x3 0xfa 0xff 0x0 0x0 +0x84 0x1 0xee 0x64 0x0 0x0 0x0 0x0 0x0 0x1 0xf0 0x65 0x0 0x0 0x0 0x0 0x0 +0x85 0x0 0xf2 0xfe 0x0 0xf 0x0 0x0 0x0 0x6 0xf2 0x14 0x3 0xf 0x0 0x0 0x0 +0x86 0x1 0xf3 0x66 0x0 0x0 0x0 0x0 0x0 0x1 0xf4 0x67 0x0 0x0 0x0 0x0 0x0 +0x87 0x1 0xf6 0x69 0x0 0x0 0x0 0x0 0x0 0x0 0xf7 0xef 0x0 0xf3 0xff 0xfe 0xff +0x88 0x6 0xf7 0x14 0x3 0xf3 0xff 0xfe 0xff 0x1 0xf8 0x6a 0x0 0x0 0x0 0x0 0x0 +0x89 0x1 0xf9 0x6b 0x0 0x0 0x0 0x0 0x0 0x1 0xfa 0x6d 0x0 0x0 0x0 0x0 0x0 +0x8a 0x0 0xfb 0xe6 0x0 0xd7 0xff 0x0 0x0 0x6 0xfb 0x14 0x3 0xd7 0xff 0x0 0x0 +0x8b 0x1 0xfc 0x6f 0x0 0x0 0x0 0x0 0x0 0x1 0xfd 0x70 0x0 0x0 0x0 0x0 0x0 +0x8c 0x0 0xfe 0xe3 0x0 0xe6 0xff 0x0 0x0 0x6 0xfe 0x14 0x3 0xe6 0xff 0x0 0x0 +0x8d 0x1 0xff 0x71 0x0 0x0 0x0 0x0 0x0 0x1 0x2 0x72 0x0 0x0 0x0 0x0 0x0 +0x8e 0x0 0x3 0xe7 0x0 0xda 0xff 0x0 0x0 0x6 0x3 0x14 0x3 0xda 0xff 0x0 0x0 +0x8f 0x1 0x4 0x73 0x0 0x0 0x0 0x0 0x0 0x1 0x6 0x72 0x0 0x0 0x0 0x0 0x0 +0x90 0x0 0x7 0xfb 0x0 0xfc 0xff 0x0 0x0 0x6 0x7 0x14 0x3 0xfc 0xff 0x0 0x0 +0x91 0x1 0x7 0x71 0x0 0x0 0x0 0x0 0x0 0x1 0x8 0x70 0x0 0x0 0x0 0x0 0x0 +0x92 0x0 0xb 0xee 0x0 0xd 0x0 0x0 0x0 0x6 0xb 0x14 0x3 0xd 0x0 0x0 0x0 +0x93 0x1 0xb 0x71 0x0 0x0 0x0 0x0 0x0 0x1 0xc 0x70 0x0 0x0 0x0 0x0 0x0 +0x94 0x1 0xd 0x6f 0x0 0x0 0x0 0x0 0x0 0x0 0xe 0xef 0x0 0x13 0x0 0x0 0x0 +0x95 0x6 0xe 0x14 0x3 0x13 0x0 0x0 0x0 0x1 0x10 0x71 0x0 0x0 0x0 0x0 0x0 +0x96 0x1 0x11 0x70 0x0 0x0 0x0 0x0 0x0 0x1 0x12 0x71 0x0 0x0 0x0 0x0 0x0 +0x97 0x0 0x14 0x6 0x1 0xf1 0xff 0x0 0x0 0x6 0x14 0x1 0x3 0xf1 0xff 0x0 0x0 +0x98 0x1 0x15 0x72 0x0 0x0 0x0 0x0 0x0 0x1 0x17 0x73 0x0 0x0 0x0 0x0 0x0 +0x99 0x0 0x18 0x7 0x1 0xa 0x0 0xff 0xff 0x6 0x18 0x1 0x3 0xa 0x0 0xff 0xff +0x9a 0x5 0x18 0x71 0x1 0xa 0x0 0xff 0xff 0x1 0x1b 0x75 0x0 0x0 0x0 0x0 0x0 +0x9b 0x0 0x1b 0x1 0x1 0x7 0x0 0x0 0x0 0x6 0x1b 0xd2 0x2 0x7 0x0 0x0 0x0 +0x9c 0x1 0x1c 0x74 0x0 0x0 0x0 0x0 0x0 0x0 0x1f 0xc 0x1 0x4 0x0 0x0 0x0 +0x9d 0x6 0x1f 0xba 0x2 0x4 0x0 0x0 0x0 0x1 0x1f 0x76 0x0 0x0 0x0 0x0 0x0 +0x9e 0x1 0x21 0x75 0x0 0x0 0x0 0x0 0x0 0x1 0x23 0x76 0x0 0x0 0x0 0x0 0x0 +0x9f 0x0 0x24 0xc7 0x0 0xb4 0xff 0x0 0x0 0x6 0x24 0xba 0x2 0xb4 0xff 0x0 0x0 +0xa0 0x1 0x24 0x75 0x0 0x0 0x0 0x0 0x0 0x0 0x27 0x78 0x0 0x8a 0xff 0x0 0x0 +0xa1 0x6 0x27 0xba 0x2 0x8a 0xff 0x0 0x0 0x1 0x28 0x74 0x0 0x0 0x0 0x0 0x0 +0xa2 0x1 0x29 0x73 0x0 0x0 0x0 0x0 0x0 0x0 0x2a 0x4b 0x0 0x7a 0xff 0x0 0x0 +0xa3 0x6 0x2a 0xba 0x2 0x7a 0xff 0x0 0x0 0x1 0x2a 0x72 0x0 0x0 0x0 0x0 0x0 +0xa4 0x0 0x2d 0x37 0x0 0x6d 0xff 0x0 0x0 0x6 0x2d 0xa1 0x2 0x6d 0xff 0x0 0x0 +0xa5 0x1 0x2d 0x71 0x0 0x0 0x0 0x0 0x0 0x1 0x2e 0x70 0x0 0x0 0x0 0x0 0x0 +0xa6 0x0 0x30 0x34 0x0 0x67 0xff 0x0 0x0 0x6 0x30 0x85 0x2 0x67 0xff 0x0 0x0 +0xa7 0x1 0x32 0x6f 0x0 0x0 0x0 0x0 0x0 0x0 0x33 0x35 0x0 0x61 0xff 0x0 0x0 +0xa8 0x6 0x33 0x70 0x2 0x61 0xff 0x0 0x0 0x1 0x33 0x70 0x0 0x0 0x0 0x0 0x0 +0xa9 0x0 0x36 0x26 0x0 0x6a 0xff 0x0 0x0 0x6 0x36 0x5e 0x2 0x6a 0xff 0x0 0x0 +0xaa 0x1 0x37 0x6f 0x0 0x0 0x0 0x0 0x0 0x0 0x39 0x28 0x0 0x68 0xff 0x0 0x0 +0xab 0x6 0x39 0x4b 0x2 0x68 0xff 0x0 0x0 0x1 0x39 0x6e 0x0 0x0 0x0 0x0 0x0 +0xac 0x1 0x3b 0x6d 0x0 0x0 0x0 0x0 0x0 0x0 0x3c 0x24 0x0 0x66 0xff 0x0 0x0 +0xad 0x6 0x3c 0x4b 0x2 0x66 0xff 0x0 0x0 0x1 0x3d 0x6b 0x0 0x0 0x0 0x0 0x0 +0xae 0x0 0x3f 0x26 0x0 0x66 0xff 0x0 0x0 0x6 0x3f 0x4b 0x2 0x66 0xff 0x0 0x0 +0xaf 0x1 0x40 0x6a 0x0 0x0 0x0 0x0 0x0 0x1 0x41 0x68 0x0 0x0 0x0 0x0 0x0 +0xb0 0x0 0x42 0x1f 0x0 0x54 0xff 0x0 0x0 0x6 0x42 0x36 0x2 0x54 0xff 0x0 0x0 +0xb1 0x1 0x43 0x67 0x0 0x0 0x0 0x0 0x0 0x0 0x45 0xf 0x0 0x4d 0xff 0x0 0x0 +0xb2 0x6 0x45 0x36 0x2 0x4d 0xff 0x0 0x0 0x1 0x45 0x68 0x0 0x0 0x0 0x0 0x0 +0xb3 0x1 0x46 0x69 0x0 0x0 0x0 0x0 0x0 0x0 0x48 0xd 0x0 0x68 0xff 0x0 0x0 +0xb4 0x6 0x48 0x23 0x2 0x68 0xff 0x0 0x0 0x0 0x4b 0x13 0x0 0x62 0xff 0x0 0x0 +0xb5 0x6 0x4b 0x23 0x2 0x62 0xff 0x0 0x0 0x1 0x4b 0x6a 0x0 0x0 0x0 0x0 0x0 +0xb6 0x1 0x4c 0x6c 0x0 0x0 0x0 0x0 0x0 0x0 0x4e 0x0 0x0 0x4d 0xff 0x0 0x0 +0xb7 0x6 0x4e 0x23 0x2 0x4d 0xff 0x0 0x0 0x1 0x50 0x6e 0x0 0x0 0x0 0x0 0x0 +0xb8 0x0 0x51 0xf4 0xff 0x5b 0xff 0x0 0x0 0x6 0x51 0x23 0x2 0x5b 0xff 0x0 0x0 +0xb9 0x1 0x51 0x6d 0x0 0x0 0x0 0x0 0x0 0x0 0x54 0x1 0x0 0x63 0xff 0x0 0x0 +0xba 0x6 0x54 0x23 0x2 0x63 0xff 0x0 0x0 0x1 0x54 0x6e 0x0 0x0 0x0 0x0 0x0 +0xbb 0x1 0x55 0x6f 0x0 0x0 0x0 0x0 0x0 0x0 0x57 0xf6 0xff 0x6e 0xff 0x0 0x0 +0xbc 0x6 0x57 0x23 0x2 0x6e 0xff 0x0 0x0 0x1 0x5a 0x6e 0x0 0x0 0x0 0x0 0x0 +0xbd 0x0 0x5c 0xe5 0xff 0x64 0xff 0x0 0x0 0x6 0x5c 0x36 0x2 0x64 0xff 0x0 0x0 +0xbe 0x1 0x5d 0x71 0x0 0x0 0x0 0x0 0x0 0x0 0x5f 0x1a 0x0 0x71 0xff 0x0 0x0 +0xbf 0x6 0x5f 0x4e 0x2 0x71 0xff 0x0 0x0 0x0 0x62 0xb 0x0 0x5c 0xff 0x0 0x0 +0xc0 0x6 0x62 0x4e 0x2 0x5c 0xff 0x0 0x0 0x1 0x62 0x70 0x0 0x0 0x0 0x0 0x0 +0xc1 0x1 0x64 0x71 0x0 0x0 0x0 0x0 0x0 0x0 0x65 0xf1 0xff 0x5e 0xff 0x0 0x0 +0xc2 0x6 0x65 0x4e 0x2 0x5e 0xff 0x0 0x0 0x1 0x65 0x72 0x0 0x0 0x0 0x0 0x0 +0xc3 0x1 0x67 0x70 0x0 0x0 0x0 0x0 0x0 0x0 0x68 0x2 0x0 0x67 0xff 0x0 0x0 +0xc4 0x6 0x68 0x3b 0x2 0x67 0xff 0x0 0x0 0x1 0x68 0x6e 0x0 0x0 0x0 0x0 0x0 +0xc5 0x0 0x6b 0x16 0x0 0x59 0xff 0x0 0x0 0x6 0x6b 0x3b 0x2 0x59 0xff 0x0 0x0 +0xc6 0x1 0x6c 0x6f 0x0 0x0 0x0 0x0 0x0 0x0 0x6e 0x23 0x0 0x6c 0xff 0x0 0x0 +0xc7 0x6 0x6e 0x3b 0x2 0x6c 0xff 0x0 0x0 0x1 0x71 0x6e 0x0 0x0 0x0 0x0 0x0 +0xc8 0x0 0x72 0x3a 0x0 0x57 0xff 0x0 0x0 0x6 0x72 0x27 0x2 0x57 0xff 0x0 0x0 +0xc9 0x1 0x72 0x70 0x0 0x0 0x0 0x0 0x0 0x0 0x75 0x16 0x0 0x5b 0xff 0x0 0x0 +0xca 0x6 0x75 0x27 0x2 0x5b 0xff 0x0 0x0 0x1 0x76 0x6f 0x0 0x0 0x0 0x0 0x0 +0xcb 0x1 0x77 0x6e 0x0 0x0 0x0 0x0 0x0 0x0 0x78 0xf7 0xff 0x5b 0xff 0x0 0x0 +0xcc 0x6 0x78 0x27 0x2 0x5b 0xff 0x0 0x0 0x4 0x78 0x9c 0x1 0x5b 0xff 0x0 0x0 +0xcd 0x1 0x78 0x6d 0x0 0x0 0x0 0x0 0x0 0x0 0x7b 0xf2 0xff 0x6d 0xff 0x0 0x0 +0xce 0x6 0x7b 0x27 0x2 0x6d 0xff 0x0 0x0 0x1 0x7c 0x6c 0x0 0x0 0x0 0x0 0x0 +0xcf 0x1 0x7d 0x6b 0x0 0x0 0x0 0x0 0x0 0x0 0x7e 0xef 0xff 0x64 0xff 0x0 0x0 +0xd0 0x6 0x7e 0x27 0x2 0x64 0xff 0x0 0x0 0x1 0x7f 0x6a 0x0 0x0 0x0 0x0 0x0 +0xd1 0x0 0x82 0xca 0xff 0x58 0xff 0x0 0x0 0x6 0x82 0x27 0x2 0x58 0xff 0x0 0x0 +0xd2 0x1 0x82 0x69 0x0 0x0 0x0 0x0 0x0 0x1 0x84 0x6a 0x0 0x0 0x0 0x0 0x0 +0xd3 0x0 0x86 0xce 0xff 0x57 0xff 0x0 0x0 0x6 0x86 0x27 0x2 0x57 0xff 0x0 0x0 +0xd4 0x0 0x89 0xfe 0xff 0x65 0xff 0x0 0x0 0x6 0x89 0x27 0x2 0x65 0xff 0x0 0x0 +0xd5 0x1 0x89 0x6b 0x0 0x0 0x0 0x0 0x0 0x1 0x8a 0x6c 0x0 0x0 0x0 0x0 0x0 +0xd6 0x1 0x8c 0x6d 0x0 0x0 0x0 0x0 0x0 0x0 0x8d 0x13 0x0 0x68 0xff 0x0 0x0 +0xd7 0x6 0x8d 0x27 0x2 0x68 0xff 0x0 0x0 0x1 0x8e 0x6c 0x0 0x0 0x0 0x0 0x0 +0xd8 0x1 0x8f 0x6b 0x0 0x0 0x0 0x0 0x0 0x0 0x91 0xf0 0xff 0x6b 0xff 0x0 0x0 +0xd9 0x6 0x91 0x54 0x2 0x6b 0xff 0x0 0x0 0x0 0x94 0xee 0xff 0x6b 0xff 0x0 0x0 +0xda 0x6 0x94 0x54 0x2 0x6b 0xff 0x0 0x0 0x1 0x94 0x6c 0x0 0x0 0x0 0x0 0x0 +0xdb 0x0 0x97 0xfc 0xff 0x6f 0xff 0x0 0x0 0x6 0x97 0x54 0x2 0x6f 0xff 0x0 0x0 +0xdc 0x1 0x98 0x6d 0x0 0x0 0x0 0x0 0x0 0x1 0x99 0x6e 0x0 0x0 0x0 0x0 0x0 +0xdd 0x0 0x9a 0x11 0x0 0x6f 0xff 0x0 0x0 0x6 0x9a 0x54 0x2 0x6f 0xff 0x0 0x0 +0xde 0x1 0x9a 0x70 0x0 0x0 0x0 0x0 0x0 0x1 0x9d 0x71 0x0 0x0 0x0 0x0 0x0 +0xdf 0x0 0x9d 0x25 0x0 0x72 0xff 0x0 0x0 0x6 0x9d 0x54 0x2 0x72 0xff 0x0 0x0 +0xe0 0x1 0x9e 0x72 0x0 0x0 0x0 0x0 0x0 0x0 0xa1 0xed 0xff 0x70 0xff 0x0 0x0 +0xe1 0x6 0xa1 0x54 0x2 0x70 0xff 0x0 0x0 0x1 0xa2 0x73 0x0 0x0 0x0 0x0 0x0 +0xe2 0x1 0xa3 0x74 0x0 0x0 0x0 0x0 0x0 0x1 0xa4 0x73 0x0 0x0 0x0 0x0 0x0 +0xe3 0x1 0xa6 0x72 0x0 0x0 0x0 0x0 0x0 0x1 0xa7 0x70 0x0 0x0 0x0 0x0 0x0 +0xe4 0x1 0xab 0x6e 0x0 0x0 0x0 0x0 0x0 0x1 0xac 0x6d 0x0 0x0 0x0 0x0 0x0 +0xe5 0x0 0xad 0x60 0x0 0xcc 0xff 0x0 0x0 0x6 0xad 0xfb 0x2 0xcc 0xff 0x0 0x0 +0xe6 0x1 0xaf 0x6e 0x0 0x0 0x0 0x0 0x0 0x1 0xb5 0x6f 0x0 0x0 0x0 0x0 0x0 +0xe7 0x1 0xb6 0x70 0x0 0x0 0x0 0x0 0x0 0x1 0xb8 0x6f 0x0 0x0 0x0 0x0 0x0 +0xe8 0x1 0xbd 0x70 0x0 0x0 0x0 0x0 0x0 0x1 0xc0 0x6f 0x0 0x0 0x0 0x0 0x0 +0xe9 0x1 0xc2 0x68 0x0 0x0 0x0 0x0 0x0 0x1 0xc3 0x67 0x0 0x0 0x0 0x0 0x0 +0xea 0x1 0xc4 0x66 0x0 0x0 0x0 0x0 0x0 0x1 0xc7 0x5f 0x0 0x0 0x0 0x0 0x0 +0xeb 0x1 0xc8 0x5e 0x0 0x0 0x0 0x0 0x0 0x1 0xc9 0x59 0x0 0x0 0x0 0x0 0x0 +0xec 0x1 0xcc 0x58 0x0 0x0 0x0 0x0 0x0 0x1 0xcd 0x57 0x0 0x0 0x0 0x0 0x0 +0xed 0x1 0xce 0x54 0x0 0x0 0x0 0x0 0x0 0x1 0xd1 0x4d 0x0 0x0 0x0 0x0 0x0 +0xee 0x1 0xd2 0x4c 0x0 0x0 0x0 0x0 0x0 0x1 0xd3 0x4a 0x0 0x0 0x0 0x0 0x0 +0xef 0x1 0xd8 0x49 0x0 0x0 0x0 0x0 0x0 0x1 0xda 0x4a 0x0 0x0 0x0 0x0 0x0 +0xf0 0x1 0xdc 0x51 0x0 0x0 0x0 0x0 0x0 0x1 0xdd 0x4a 0x0 0x0 0x0 0x0 0x0 +0xf1 0x1 0xdf 0x51 0x0 0x0 0x0 0x0 0x0 0x1 0xe0 0x52 0x0 0x0 0x0 0x0 0x0 +0xf2 0x1 0xe2 0x53 0x0 0x0 0x0 0x0 0x0 0x1 0xe4 0x5a 0x0 0x0 0x0 0x0 0x0 +0xf3 0x1 0xe5 0x56 0x0 0x0 0x0 0x0 0x0 0x1 0xe7 0x54 0x0 0x0 0x0 0x0 0x0 +0xf4 0x1 0xe9 0x5a 0x0 0x0 0x0 0x0 0x0 0x1 0xea 0x58 0x0 0x0 0x0 0x0 0x0 +0xf5 0x1 0xeb 0x56 0x0 0x0 0x0 0x0 0x0 0x1 0xee 0x54 0x0 0x0 0x0 0x0 0x0 +0xf6 0x1 0xef 0x55 0x0 0x0 0x0 0x0 0x0 0x1 0xf4 0x56 0x0 0x0 0x0 0x0 0x0 +0xf7 0x1 0xf5 0x57 0x0 0x0 0x0 0x0 0x0 0x1 0xf8 0x58 0x0 0x0 0x0 0x0 0x0 +0xf8 0x1 0xfa 0x5a 0x0 0x0 0x0 0x0 0x0 0x1 0xff 0x59 0x0 0x0 0x0 0x0 0x0 +0xf9 0x1 0x1 0x55 0x0 0x0 0x0 0x0 0x0 0x1 0x3 0x58 0x0 0x0 0x0 0x0 0x0 +0xfa 0x1 0x4 0x59 0x0 0x0 0x0 0x0 0x0 0x1 0x6 0x60 0x0 0x0 0x0 0x0 0x0 +0xfb 0x1 0x7 0x61 0x0 0x0 0x0 0x0 0x0 0x1 0x9 0x68 0x0 0x0 0x0 0x0 0x0 +0xfc 0x1 0xc 0x6e 0x0 0x0 0x0 0x0 0x0 0x1 0x10 0x6f 0x0 0x0 0x0 0x0 0x0 +0xfd 0x1 0x11 0x6e 0x0 0x0 0x0 0x0 0x0 0x1 0x12 0x6f 0x0 0x0 0x0 0x0 0x0 +0xfe 0x1 0x15 0x70 0x0 0x0 0x0 0x0 0x0 0x1 0x16 0x6f 0x0 0x0 0x0 0x0 0x0 +0xff 0x1 0x17 0x6e 0x0 0x0 0x0 0x0 0x0 0x1 0x1c 0x6d 0x0 0x0 0x0 0x0 0x0 +0x0 0x1 0x1f 0x6b 0x0 0x0 0x0 0x0 0x0 0x1 0x21 0x6a 0x0 0x0 0x0 0x0 0x0 +0x1 0x1 0x25 0x6b 0x0 0x0 0x0 0x0 0x0 0x1 0x26 0x6c 0x0 0x0 0x0 0x0 0x0 +0x2 0x1 0x28 0x6e 0x0 0x0 0x0 0x0 0x0 0x1 0x2a 0x6f 0x0 0x0 0x0 0x0 0x0 +0x3 0x1 0x2e 0x6e 0x0 0x0 0x0 0x0 0x0 0x1 0x30 0x6d 0x0 0x0 0x0 0x0 0x0 +0x4 0x1 0x32 0x6b 0x0 0x0 0x0 0x0 0x0 0x1 0x33 0x6a 0x0 0x0 0x0 0x0 0x0 +0x5 0x1 0x34 0x69 0x0 0x0 0x0 0x0 0x0 0x1 0x38 0x68 0x0 0x0 0x0 0x0 0x0 +0x6 0x1 0x39 0x67 0x0 0x0 0x0 0x0 0x0 0x1 0x3d 0x66 0x0 0x0 0x0 0x0 0x0 +0x7 0x1 0x3e 0x67 0x0 0x0 0x0 0x0 0x0 0x1 0x41 0x6e 0x0 0x0 0x0 0x0 0x0 +0x8 0x1 0x42 0x6f 0x0 0x0 0x0 0x0 0x0 0x1 0x43 0x68 0x0 0x0 0x0 0x0 0x0 +0x9 0x0 0x44 0x9f 0x2 0x56 0xfe 0x0 0x0 0x6 0x44 0x4 0x4 0x56 0xfe 0x0 0x0 +0xa 0x0 0x45 0xfd 0xff 0x2 0x0 0x1 0x0 0x6 0x45 0x4 0x4 0x2 0x0 0x1 0x0 +0xb 0x1 0x46 0x67 0x0 0x0 0x0 0x0 0x0 0x1 0x47 0x6c 0x0 0x0 0x0 0x0 0x0 +0xc 0x1 0x48 0x6b 0x0 0x0 0x0 0x0 0x0 0x1 0x4a 0x6a 0x0 0x0 0x0 0x0 0x0 +0xd 0x1 0x4c 0x67 0x0 0x0 0x0 0x0 0x0 0x1 0x4d 0x68 0x0 0x0 0x0 0x0 0x0 +0xe 0x1 0x4f 0x6b 0x0 0x0 0x0 0x0 0x0 0x1 0x50 0x6a 0x0 0x0 0x0 0x0 0x0 +0xf 0x1 0x52 0x69 0x0 0x0 0x0 0x0 0x0 0x1 0x54 0x68 0x0 0x0 0x0 0x0 0x0 +0x10 0x1 0x59 0x68 0x0 0x0 0x0 0x0 0x0 0x1 0x5a 0x67 0x0 0x0 0x0 0x0 0x0 +0x11 0x1 0x5b 0x66 0x0 0x0 0x0 0x0 0x0 0x1 0x5e 0x65 0x0 0x0 0x0 0x0 0x0 +0x12 0x1 0x5f 0x64 0x0 0x0 0x0 0x0 0x0 0x1 0x64 0x63 0x0 0x0 0x0 0x0 0x0 +0x13 0x1 0x68 0x64 0x0 0x0 0x0 0x0 0x0 0x1 0x69 0x65 0x0 0x0 0x0 0x0 0x0 +0x14 0x1 0x6e 0x66 0x0 0x0 0x0 0x0 0x0 0x1 0x6f 0x68 0x0 0x0 0x0 0x0 0x0 +0x15 0x1 0x74 0x68 0x0 0x0 0x0 0x0 0x0 0x1 0x76 0x67 0x0 0x0 0x0 0x0 0x0 +0x16 0x1 0x77 0x66 0x0 0x0 0x0 0x0 0x0 0x1 0x7c 0x65 0x0 0x0 0x0 0x0 0x0 +0x17 0x1 0x7e 0x66 0x0 0x0 0x0 0x0 0x0 0x1 0x85 0x64 0x0 0x0 0x0 0x0 0x0 +0x18 0x1 0x86 0x65 0x0 0x0 0x0 0x0 0x0 0x1 0x87 0x64 0x0 0x0 0x0 0x0 0x0 +0x19 0x1 0x8a 0x63 0x0 0x0 0x0 0x0 0x0 0x1 0x8c 0x61 0x0 0x0 0x0 0x0 0x0 +0x1a 0x1 0x90 0x60 0x0 0x0 0x0 0x0 0x0 0x1 0x91 0x5f 0x0 0x0 0x0 0x0 0x0 +0x1b 0x1 0x93 0x5e 0x0 0x0 0x0 0x0 0x0 0x1 0x95 0x5d 0x0 0x0 0x0 0x0 0x0 +0x1c 0x1 0x96 0x5e 0x0 0x0 0x0 0x0 0x0 0x1 0x9b 0x5d 0x0 0x0 0x0 0x0 0x0 +0x1d 0x1 0x9d 0x5f 0x0 0x0 0x0 0x0 0x0 0x1 0x9e 0x5e 0x0 0x0 0x0 0x0 0x0 +0x1e 0x1 0xa0 0x65 0x0 0x0 0x0 0x0 0x0 0x1 0xa2 0x5e 0x0 0x0 0x0 0x0 0x0 +0x1f 0x1 0xa3 0x5d 0x0 0x0 0x0 0x0 0x0 0x1 0xa5 0x56 0x0 0x0 0x0 0x0 0x0 +0x20 0x1 0xa7 0x52 0x0 0x0 0x0 0x0 0x0 0x1 0xa8 0x51 0x0 0x0 0x0 0x0 0x0 +0x21 0x1 0xa9 0x4e 0x0 0x0 0x0 0x0 0x0 0x1 0xac 0x4d 0x0 0x0 0x0 0x0 0x0 +0x22 0x1 0xad 0x4c 0x0 0x0 0x0 0x0 0x0 0x1 0xae 0x4b 0x0 0x0 0x0 0x0 0x0 +0x23 0x1 0xb1 0x4a 0x0 0x0 0x0 0x0 0x0 0x1 0xb2 0x4c 0x0 0x0 0x0 0x0 0x0 +0x24 0x1 0xb3 0x4b 0x0 0x0 0x0 0x0 0x0 0x1 0xb6 0x4a 0x0 0x0 0x0 0x0 0x0 +0x25 0x1 0xb8 0x4b 0x0 0x0 0x0 0x0 0x0 0x1 0xba 0x4c 0x0 0x0 0x0 0x0 0x0 +0x26 0x1 0xbc 0x4d 0x0 0x0 0x0 0x0 0x0 0x1 0xbd 0x4e 0x0 0x0 0x0 0x0 0x0 +0x27 0x1 0xc1 0x50 0x0 0x0 0x0 0x0 0x0 0x1 0xc2 0x51 0x0 0x0 0x0 0x0 0x0 +0x28 0x1 0xc4 0x52 0x0 0x0 0x0 0x0 0x0 0x1 0xc7 0x53 0x0 0x0 0x0 0x0 0x0 +0x29 0x1 0xc9 0x54 0x0 0x0 0x0 0x0 0x0 0x1 0xce 0x55 0x0 0x0 0x0 0x0 0x0 +0x2a 0x1 0xcf 0x56 0x0 0x0 0x0 0x0 0x0 0x1 0xd0 0x55 0x0 0x0 0x0 0x0 0x0 +0x2b 0x1 0xd3 0x56 0x0 0x0 0x0 0x0 0x0 0x1 0xd4 0x57 0x0 0x0 0x0 0x0 0x0 +0x2c 0x1 0xd5 0x54 0x0 0x0 0x0 0x0 0x0 0x1 0xd8 0x53 0x0 0x0 0x0 0x0 0x0 +0x2d 0x1 0xd9 0x55 0x0 0x0 0x0 0x0 0x0 0x1 0xdd 0x54 0x0 0x0 0x0 0x0 0x0 +0x2e 0x1 0xdf 0x55 0x0 0x0 0x0 0x0 0x0 0x1 0xe1 0x56 0x0 0x0 0x0 0x0 0x0 +0x2f 0x1 0xe3 0x57 0x0 0x0 0x0 0x0 0x0 0x1 0xe6 0x59 0x0 0x0 0x0 0x0 0x0 +0x30 0x1 0xe8 0x5a 0x0 0x0 0x0 0x0 0x0 0x1 0xe9 0x5b 0x0 0x0 0x0 0x0 0x0 +0x31 0x1 0xeb 0x5a 0x0 0x0 0x0 0x0 0x0 0x1 0xec 0x53 0x0 0x0 0x0 0x0 0x0 +0x32 0x1 0xee 0x52 0x0 0x0 0x0 0x0 0x0 0x1 0xf0 0x51 0x0 0x0 0x0 0x0 0x0 +0x33 0x1 0xf1 0x4a 0x0 0x0 0x0 0x0 0x0 0x1 0xf3 0x49 0x0 0x0 0x0 0x0 0x0 +0x34 0x1 0xf5 0x42 0x0 0x0 0x0 0x0 0x0 0x1 0xf6 0x43 0x0 0x0 0x0 0x0 0x0 +0x35 0x1 0xf7 0x44 0x0 0x0 0x0 0x0 0x0 0x1 0xfa 0x4b 0x0 0x0 0x0 0x0 0x0 +0x36 0x1 0xfb 0x51 0x0 0x0 0x0 0x0 0x0 0x1 0xfc 0x50 0x0 0x0 0x0 0x0 0x0 +0x37 0x1 0xff 0x4f 0x0 0x0 0x0 0x0 0x0 0x1 0x0 0x50 0x0 0x0 0x0 0x0 0x0 +0x38 0x1 0x1 0x51 0x0 0x0 0x0 0x0 0x0 0x1 0x5 0x50 0x0 0x0 0x0 0x0 0x0 +0x39 0x1 0x8 0x53 0x0 0x0 0x0 0x0 0x0 0x1 0xa 0x51 0x0 0x0 0x0 0x0 0x0 +0x3a 0x1 0xf 0x51 0x0 0x0 0x0 0x0 0x0 0x1 0x12 0x53 0x0 0x0 0x0 0x0 0x0 +0x3b 0x1 0x13 0x5a 0x0 0x0 0x0 0x0 0x0 0x1 0x15 0x5f 0x0 0x0 0x0 0x0 0x0 +0x3c 0x1 0x17 0x64 0x0 0x0 0x0 0x0 0x0 0x1 0x18 0x5e 0x0 0x0 0x0 0x0 0x0 +0x3d 0x1 0x19 0x5f 0x0 0x0 0x0 0x0 0x0 0x1 0x1c 0x60 0x0 0x0 0x0 0x0 0x0 +0x3e 0x1 0x1e 0x5f 0x0 0x0 0x0 0x0 0x0 0x1 0x21 0x5d 0x0 0x0 0x0 0x0 0x0 +0x3f 0x1 0x22 0x5c 0x0 0x0 0x0 0x0 0x0 0x1 0x23 0x5d 0x0 0x0 0x0 0x0 0x0 +0x40 0x1 0x26 0x5e 0x0 0x0 0x0 0x0 0x0 0x1 0x27 0x5f 0x0 0x0 0x0 0x0 0x0 +0x41 0x1 0x2b 0x5e 0x0 0x0 0x0 0x0 0x0 0x1 0x2f 0x5d 0x0 0x0 0x0 0x0 0x0 +0x42 0x1 0x31 0x5c 0x0 0x0 0x0 0x0 0x0 0x1 0x32 0x5a 0x0 0x0 0x0 0x0 0x0 +0x43 0x1 0x34 0x5b 0x0 0x0 0x0 0x0 0x0 0x1 0x35 0x5c 0x0 0x0 0x0 0x0 0x0 +0x44 0x1 0x37 0x5d 0x0 0x0 0x0 0x0 0x0 0x1 0x39 0x5e 0x0 0x0 0x0 0x0 0x0 +0x45 0x1 0x3a 0x5f 0x0 0x0 0x0 0x0 0x0 0x1 0x3b 0x61 0x0 0x0 0x0 0x0 0x0 +0x46 0x1 0x3e 0x63 0x0 0x0 0x0 0x0 0x0 0x1 0x3f 0x64 0x0 0x0 0x0 0x0 0x0 +0x47 0x1 0x43 0x63 0x0 0x0 0x0 0x0 0x0 0x1 0x44 0x64 0x0 0x0 0x0 0x0 0x0 +0x48 0x1 0x45 0x63 0x0 0x0 0x0 0x0 0x0 0x1 0x47 0x62 0x0 0x0 0x0 0x0 0x0 +0x49 0x1 0x49 0x61 0x0 0x0 0x0 0x0 0x0 0x1 0x4e 0x60 0x0 0x0 0x0 0x0 0x0 +0x4a 0x1 0x4f 0x61 0x0 0x0 0x0 0x0 0x0 0x1 0x51 0x60 0x0 0x0 0x0 0x0 0x0 +0x4b 0x1 0x52 0x61 0x0 0x0 0x0 0x0 0x0 0x1 0x56 0x62 0x0 0x0 0x0 0x0 0x0 +0x4c 0x1 0x57 0x63 0x0 0x0 0x0 0x0 0x0 0x1 0x58 0x62 0x0 0x0 0x0 0x0 0x0 +0x4d 0x1 0x5b 0x64 0x0 0x0 0x0 0x0 0x0 0x1 0x5d 0x65 0x0 0x0 0x0 0x0 0x0 +0x4e 0x1 0x60 0x66 0x0 0x0 0x0 0x0 0x0 0x1 0x62 0x67 0x0 0x0 0x0 0x0 0x0 +0x4f 0x1 0x65 0x66 0x0 0x0 0x0 0x0 0x0 0x1 0x66 0x60 0x0 0x0 0x0 0x0 0x0 +0x50 0x1 0x67 0x5a 0x0 0x0 0x0 0x0 0x0 0x1 0x69 0x57 0x0 0x0 0x0 0x0 0x0 +0x51 0x1 0x6b 0x50 0x0 0x0 0x0 0x0 0x0 0x1 0x6c 0x57 0x0 0x0 0x0 0x0 0x0 +0x52 0x1 0x6e 0x56 0x0 0x0 0x0 0x0 0x0 0x1 0x71 0x57 0x0 0x0 0x0 0x0 0x0 +0x53 0x1 0x73 0x58 0x0 0x0 0x0 0x0 0x0 0x1 0x74 0x5b 0x0 0x0 0x0 0x0 0x0 +0x54 0x1 0x78 0x5a 0x0 0x0 0x0 0x0 0x0 0x1 0x7d 0x5a 0x0 0x0 0x0 0x0 0x0 +0x55 0x1 0x82 0x5a 0x0 0x0 0x0 0x0 0x0 0x1 0x83 0x5b 0x0 0x0 0x0 0x0 0x0 +0x56 0x1 0x87 0x5a 0x0 0x0 0x0 0x0 0x0 0x1 0x88 0x59 0x0 0x0 0x0 0x0 0x0 +0x57 0x1 0x8b 0x58 0x0 0x0 0x0 0x0 0x0 0x1 0x8d 0x57 0x0 0x0 0x0 0x0 0x0 +0x58 0x1 0x8e 0x56 0x0 0x0 0x0 0x0 0x0 0x1 0x90 0x57 0x0 0x0 0x0 0x0 0x0 +0x59 0x1 0x93 0x55 0x0 0x0 0x0 0x0 0x0 0x1 0x96 0x54 0x0 0x0 0x0 0x0 0x0 +0x5a 0x1 0x98 0x55 0x0 0x0 0x0 0x0 0x0 0x1 0x9c 0x56 0x0 0x0 0x0 0x0 0x0 +0x5b 0x1 0x9f 0x57 0x0 0x0 0x0 0x0 0x0 0x1 0xa0 0x58 0x0 0x0 0x0 0x0 0x0 +0x5c 0x1 0xa4 0x59 0x0 0x0 0x0 0x0 0x0 0x2 0xa8 0x0 0x0 0x0 0x0 0x0 0x0 From 7b3556756aa646a50f5b6b6ee04422fb61ba31a7 Mon Sep 17 00:00:00 2001 From: cpfeiffer Date: Tue, 31 Oct 2017 23:39:49 +0100 Subject: [PATCH 03/17] Bip: activity types are different from samples' activity kind --- .../devices/miband2/MiBand2Const.java | 2 +- .../gadgetbridge/model/ActivityKind.java | 11 +++--- .../devices/amazfitbip/BipActivityType.java | 38 +++++++++++++++++++ .../FetchSportsSummaryOperation.java | 12 ++++-- 4 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/BipActivityType.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband2/MiBand2Const.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband2/MiBand2Const.java index 1e3585719..a153dda97 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband2/MiBand2Const.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband2/MiBand2Const.java @@ -42,7 +42,7 @@ public class MiBand2Const { case TYPE_CHARGING: return ActivityKind.TYPE_NOT_WORN; //I believe it's a safe assumption case TYPE_RIDE_BIKE: - return ActivityKind.TYPE_BIKING; + return ActivityKind.TYPE_CYCLING; default: case TYPE_UNSET: // fall through return ActivityKind.TYPE_UNKNOWN; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityKind.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityKind.java index b8108bebf..fc610aa29 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityKind.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityKind.java @@ -35,10 +35,11 @@ public class ActivityKind { public static final int TYPE_RUNNING = 16; public static final int TYPE_WALKING = 32; public static final int TYPE_SWIMMING = 64; + public static final int TYPE_CYCLING = 128; + public static final int TYPE_TREADMILL = 256; public static final int TYPE_SLEEP = TYPE_LIGHT_SLEEP | TYPE_DEEP_SLEEP; public static final int TYPE_ALL = TYPE_ACTIVITY | TYPE_SLEEP | TYPE_NOT_WORN; - public static final int TYPE_BIKING = 128; public static int[] mapToDBActivityTypes(int types, SampleProvider provider) { int[] result = new int[3]; @@ -64,8 +65,8 @@ public class ActivityKind { if ((types & ActivityKind.TYPE_SWIMMING) != 0) { result[i++] = provider.toRawActivityKind(TYPE_SWIMMING); } - if ((types & ActivityKind.TYPE_BIKING) != 0) { - result[i++] = provider.toRawActivityKind(TYPE_BIKING); + if ((types & ActivityKind.TYPE_CYCLING) != 0) { + result[i++] = provider.toRawActivityKind(TYPE_CYCLING); } return Arrays.copyOf(result, i); } @@ -88,7 +89,7 @@ public class ActivityKind { return context.getString(R.string.activity_type_walking); case TYPE_SWIMMING: return context.getString(R.string.activity_type_swimming); - case TYPE_BIKING: + case TYPE_CYCLING: return context.getString(R.string.activity_type_biking); case TYPE_UNKNOWN: default: @@ -109,7 +110,7 @@ public class ActivityKind { return R.drawable.ic_activity_running; case TYPE_WALKING: return R.drawable.ic_activity_walking; - case TYPE_BIKING: + case TYPE_CYCLING: return R.drawable.ic_activity_biking; case TYPE_SWIMMING: // fall through case TYPE_NOT_WORN: // fall through diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/BipActivityType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/BipActivityType.java new file mode 100644 index 000000000..4122ac832 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/BipActivityType.java @@ -0,0 +1,38 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.amazfitbip; + +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; + +public enum BipActivityType { + Outdoor, + Treadmill, + Cycling, + Walking; + + public int toActivityKind() { + switch (this) { + case Outdoor: + return ActivityKind.TYPE_RUNNING; + case Treadmill: + return ActivityKind.TYPE_TREADMILL; + case Cycling: + return ActivityKind.TYPE_CYCLING; + case Walking: + return ActivityKind.TYPE_WALKING; + } + throw new RuntimeException("Not mapped activity kind for: " + this); + } + + public static BipActivityType fromActivityKind(int kind) { + switch (kind) { + case ActivityKind.TYPE_RUNNING: + return Outdoor; + case ActivityKind.TYPE_TREADMILL: + return Treadmill; + case ActivityKind.TYPE_CYCLING: + return Cycling; + case ActivityKind.TYPE_WALKING: + return Walking; + } + throw new RuntimeException("No matching activity kind: " + kind); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsSummaryOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsSummaryOperation.java index ebb0d2534..a814d35d1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsSummaryOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsSummaryOperation.java @@ -47,6 +47,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WaitAction; +import nodomain.freeyourgadget.gadgetbridge.service.devices.amazfitbip.BipActivityType; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.MiBand2Support; import nodomain.freeyourgadget.gadgetbridge.util.GB; @@ -175,10 +176,13 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation { ByteBuffer buffer = ByteBuffer.wrap(stream.toByteArray()).order(ByteOrder.LITTLE_ENDIAN); // summary.setVersion(BLETypeConversions.toUnsigned(buffer.getShort())); buffer.getShort(); // version - int rawKind = BLETypeConversions.toUnsigned(buffer.getShort()); - int activityKind = MiBand2Const.toActivityKind(rawKind); - if (activityKind == ActivityKind.TYPE_UNKNOWN) { - activityKind = rawKind; // hack for later activity kind detection + int activityKind = ActivityKind.TYPE_UNKNOWN; + try { + int rawKind = BLETypeConversions.toUnsigned(buffer.getShort()); + BipActivityType activityType = BipActivityType.values()[rawKind]; + activityKind = activityType.toActivityKind(); + } catch (Exception ex) { + LOG.error("Error mapping acivity kind: " + ex.getMessage(), ex); } summary.setActivityKind(activityKind); // FIXME: should save timezone etc. From 923d7ae09d6fd0072faa2ef2c31c16fbe2ca877d Mon Sep 17 00:00:00 2001 From: cpfeiffer Date: Wed, 1 Nov 2017 20:02:30 +0100 Subject: [PATCH 04/17] Bip: fix activity track kind constants --- .../devices/amazfitbip/BipActivityType.java | 29 ++++++++++++++----- .../FetchSportsSummaryOperation.java | 2 +- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/BipActivityType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/BipActivityType.java index 4122ac832..2a57bc9a8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/BipActivityType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/BipActivityType.java @@ -3,10 +3,16 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.amazfitbip; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; public enum BipActivityType { - Outdoor, - Treadmill, - Cycling, - Walking; + Outdoor(1), + Treadmill(2), + Cycling(3), + Walking(4); + + private final int code; + + BipActivityType(final int code) { + this.code = code; + } public int toActivityKind() { switch (this) { @@ -22,8 +28,17 @@ public enum BipActivityType { throw new RuntimeException("Not mapped activity kind for: " + this); } - public static BipActivityType fromActivityKind(int kind) { - switch (kind) { + public static BipActivityType fromCode(int bipCode) { + for (BipActivityType type : values()) { + if (type.code == bipCode) { + return type; + } + } + throw new RuntimeException("No matching BipActivityType for code: " + bipCode); + } + + public static BipActivityType fromActivityKind(int activityKind) { + switch (activityKind) { case ActivityKind.TYPE_RUNNING: return Outdoor; case ActivityKind.TYPE_TREADMILL: @@ -33,6 +48,6 @@ public enum BipActivityType { case ActivityKind.TYPE_WALKING: return Walking; } - throw new RuntimeException("No matching activity kind: " + kind); + throw new RuntimeException("No matching activity activityKind: " + activityKind); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsSummaryOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsSummaryOperation.java index a814d35d1..dea945620 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsSummaryOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsSummaryOperation.java @@ -179,7 +179,7 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation { int activityKind = ActivityKind.TYPE_UNKNOWN; try { int rawKind = BLETypeConversions.toUnsigned(buffer.getShort()); - BipActivityType activityType = BipActivityType.values()[rawKind]; + BipActivityType activityType = BipActivityType.fromCode(rawKind); activityKind = activityType.toActivityKind(); } catch (Exception ex) { LOG.error("Error mapping acivity kind: " + ex.getMessage(), ex); From 56e835771ea4c73e260f52811d782e9ef1917133 Mon Sep 17 00:00:00 2001 From: cpfeiffer Date: Wed, 1 Nov 2017 20:22:44 +0100 Subject: [PATCH 05/17] Bip: update schema version for BaseActivitySummary again --- .../freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 99b2dacbe..a27d2474e 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -45,7 +45,7 @@ public class GBDaoGenerator { public static void main(String[] args) throws Exception { - Schema schema = new Schema(17, MAIN_PACKAGE + ".entities"); + Schema schema = new Schema(18, MAIN_PACKAGE + ".entities"); Entity userAttributes = addUserAttributes(schema); Entity user = addUserInfo(schema, userAttributes); From fd159b76039872a29a01b4bff91ec95f88250c62 Mon Sep 17 00:00:00 2001 From: cpfeiffer Date: Wed, 1 Nov 2017 23:04:52 +0100 Subject: [PATCH 06/17] Bip: improved logging, read expected number of bytes for progress monitoring during sync --- .../service/btle/AbstractBTLEOperation.java | 17 +++++++++++++++++ .../service/btle/BTLEOperation.java | 5 +++++ .../AmazfitBipFetchLogsOperation.java | 6 +++--- .../operations/AbstractFetchOperation.java | 9 ++++++--- .../operations/FetchActivityOperation.java | 11 ++++++----- .../operations/FetchSportsDetailsOperation.java | 12 +++++++----- .../operations/FetchSportsSummaryOperation.java | 11 +++++------ .../gadgetbridge/test/Tryout.java | 5 +++++ 8 files changed, 54 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEOperation.java index 3b9e847ab..7d9761961 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEOperation.java @@ -43,6 +43,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.Op public abstract class AbstractBTLEOperation implements GattCallback, BTLEOperation { private final T mSupport; protected OperationStatus operationStatus = OperationStatus.INITIAL; + private String name; protected AbstractBTLEOperation(T support) { mSupport = support; @@ -115,6 +116,22 @@ public abstract class AbstractBTLEOperation return mSupport.getDevice(); } + protected void setName(String name) { + this.name = name; + } + + @Override + public String getName() { + if (name != null) { + return name; + } + String busyTask = getDevice().getBusyTask(); + if (busyTask != null) { + return busyTask; + } + return getClass().getSimpleName(); + } + protected BluetoothGattCharacteristic getCharacteristic(UUID uuid) { return mSupport.getCharacteristic(uuid); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BTLEOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BTLEOperation.java index b6efc50c3..867e9c516 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BTLEOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BTLEOperation.java @@ -20,4 +20,9 @@ import java.io.IOException; public interface BTLEOperation { void perform() throws IOException; + + /** + * Returns a human readable name of this operation, to be used e.g. in log output. + */ + String getName(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/operations/AmazfitBipFetchLogsOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/operations/AmazfitBipFetchLogsOperation.java index dd47022d4..2c7e3fed2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/operations/AmazfitBipFetchLogsOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/operations/AmazfitBipFetchLogsOperation.java @@ -26,7 +26,6 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.text.SimpleDateFormat; -import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; @@ -50,6 +49,7 @@ public class AmazfitBipFetchLogsOperation extends AbstractFetchOperation { public AmazfitBipFetchLogsOperation(AmazfitBipSupport support) { super(support); + setName("fetch logs"); } @Override @@ -90,7 +90,7 @@ public class AmazfitBipFetchLogsOperation extends AbstractFetchOperation { @Override protected void handleActivityFetchFinish(boolean success) { - LOG.info("Fetching log data has finished"); + LOG.info(getName() +" data has finished"); try { logOutputStream.close(); logOutputStream = null; @@ -113,7 +113,7 @@ public class AmazfitBipFetchLogsOperation extends AbstractFetchOperation { lastPacketCounter++; bufferActivityData(value); } else { - GB.toast("Error fetching activity data, invalid package counter: " + value[0], Toast.LENGTH_LONG, GB.ERROR); + GB.toast("Error " + getName() + " invalid package counter: " + value[0], Toast.LENGTH_LONG, GB.ERROR); handleActivityFetchFinish(false); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/AbstractFetchOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/AbstractFetchOperation.java index 2e399e1e6..9b28c449e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/AbstractFetchOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/AbstractFetchOperation.java @@ -57,6 +57,7 @@ public abstract class AbstractFetchOperation extends AbstractMiBand2Operation { protected BluetoothGattCharacteristic characteristicActivityData; protected BluetoothGattCharacteristic characteristicFetch; protected Calendar startTimestamp; + protected int expectedDataLength; public AbstractFetchOperation(MiBand2Support support) { super(support); @@ -66,7 +67,8 @@ public abstract class AbstractFetchOperation extends AbstractMiBand2Operation { protected void enableNeededNotifications(TransactionBuilder builder, boolean enable) { if (!enable) { // dynamically enabled, but always disabled on finish - builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_5_ACTIVITY_DATA), enable); + builder.notify(characteristicFetch, enable); + builder.notify(characteristicActivityData, enable); } } @@ -78,7 +80,7 @@ public abstract class AbstractFetchOperation extends AbstractMiBand2Operation { protected void startFetching() throws IOException { lastPacketCounter = -1; - TransactionBuilder builder = performInitialized("fetching activity data"); + TransactionBuilder builder = performInitialized(getName()); getSupport().setLowLatency(builder); if (fetchCount == 0) { builder.add(new SetDeviceBusyAction(getDevice(), getContext().getString(R.string.busy_task_fetch_activity_data), getContext())); @@ -139,7 +141,8 @@ public abstract class AbstractFetchOperation extends AbstractMiBand2Operation { // first two bytes are whether our request was accepted if (ArrayUtils.equals(value, MiBand2Service.RESPONSE_ACTIVITY_DATA_START_DATE_SUCCESS, 0)) { // the third byte (0x01 on success) = ? - // the 4th - 7th bytes probably somehow represent the number of bytes/packets to expect + // the 4th - 7th bytes epresent the number of bytes/packets to expect, excluding the counter bytes + expectedDataLength = BLETypeConversions.toUint32(Arrays.copyOfRange(value, 3, 7)); // last 8 bytes are the start date Calendar startTimestamp = getSupport().fromTimeBytes(Arrays.copyOfRange(value, 7, value.length)); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchActivityOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchActivityOperation.java index 8f9203012..91c008b51 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchActivityOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchActivityOperation.java @@ -57,6 +57,7 @@ public class FetchActivityOperation extends AbstractFetchOperation { public FetchActivityOperation(MiBand2Support support) { super(support); + setName("fetching activity data"); } @Override @@ -75,14 +76,14 @@ public class FetchActivityOperation extends AbstractFetchOperation { } protected void handleActivityFetchFinish(boolean success) { - LOG.info("Fetching activity data has finished round " + fetchCount); + LOG.info(getName() + " has finished round " + fetchCount); GregorianCalendar lastSyncTimestamp = saveSamples(); if (lastSyncTimestamp != null && needsAnotherFetch(lastSyncTimestamp)) { try { startFetching(); return; } catch (IOException ex) { - LOG.error("Error starting another round of fetching activity data", ex); + LOG.error("Error starting another round of " + getName(), ex); } } @@ -91,7 +92,7 @@ public class FetchActivityOperation extends AbstractFetchOperation { private boolean needsAnotherFetch(GregorianCalendar lastSyncTimestamp) { if (fetchCount > 5) { - LOG.warn("Already jave 5 fetch rounds, not doing another one."); + LOG.warn("Already have 5 fetch rounds, not doing another one."); return false; } @@ -166,12 +167,12 @@ public class FetchActivityOperation extends AbstractFetchOperation { lastPacketCounter++; bufferActivityData(value); } else { - GB.toast("Error fetching activity data, invalid package counter: " + value[0], Toast.LENGTH_LONG, GB.ERROR); + GB.toast("Error " + getName() + ", invalid package counter: " + value[0], Toast.LENGTH_LONG, GB.ERROR); handleActivityFetchFinish(false); return; } } else { - GB.toast("Error fetching activity data, unexpected package length: " + value.length, Toast.LENGTH_LONG, GB.ERROR); + GB.toast("Error " + getName() + ", unexpected package length: " + value.length, Toast.LENGTH_LONG, GB.ERROR); handleActivityFetchFinish(false); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsDetailsOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsDetailsOperation.java index 9ea748525..259951212 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsDetailsOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsDetailsOperation.java @@ -56,11 +56,13 @@ public class FetchSportsDetailsOperation extends AbstractFetchOperation { public FetchSportsDetailsOperation(BaseActivitySummary summary, MiBand2Support support) { super(support); + setName("fetching sport details"); this.summary = summary; } @Override protected void startFetching(TransactionBuilder builder) { + LOG.info("start " + getName()); buffer = new ByteArrayOutputStream(1024); GregorianCalendar sinceWhen = getLastSuccessfulSyncTime(); builder.write(characteristicFetch, BLETypeConversions.join(new byte[] { @@ -74,7 +76,7 @@ public class FetchSportsDetailsOperation extends AbstractFetchOperation { @Override protected void handleActivityFetchFinish(boolean success) { - LOG.info("Fetching activity data has finished round " + fetchCount); + LOG.info(getName() + " has finished round " + fetchCount); // GregorianCalendar lastSyncTimestamp = saveSamples(); // if (lastSyncTimestamp != null && needsAnotherFetch(lastSyncTimestamp)) { // try { @@ -126,16 +128,16 @@ public class FetchSportsDetailsOperation extends AbstractFetchOperation { */ @Override protected void handleActivityNotif(byte[] value) { - LOG.warn("sports data: " + Logging.formatBytes(value)); + LOG.warn("sports details: " + Logging.formatBytes(value)); if (!isOperationRunning()) { - LOG.error("ignoring activity data notification because operation is not running. Data length: " + value.length); + LOG.error("ignoring sports details notification because operation is not running. Data length: " + value.length); getSupport().logMessageContent(value); return; } if (value.length < 2) { - LOG.error("unexpected sports summary data length: " + value.length); + LOG.error("unexpected sports details data length: " + value.length); getSupport().logMessageContent(value); return; } @@ -144,7 +146,7 @@ public class FetchSportsDetailsOperation extends AbstractFetchOperation { lastPacketCounter++; bufferActivityData(value); } else { - GB.toast("Error fetching activity data, invalid package counter: " + value[0] + ", last was: " + lastPacketCounter, Toast.LENGTH_LONG, GB.ERROR); + GB.toast("Error " + getName() + ", invalid package counter: " + value[0] + ", last was: " + lastPacketCounter, Toast.LENGTH_LONG, GB.ERROR); handleActivityFetchFinish(false); return; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsSummaryOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsSummaryOperation.java index dea945620..d6c4b8a00 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsSummaryOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsSummaryOperation.java @@ -38,7 +38,6 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipService; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service; -import nodomain.freeyourgadget.gadgetbridge.devices.miband2.MiBand2Const; import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; @@ -60,14 +59,14 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation { private ByteArrayOutputStream buffer = new ByteArrayOutputStream(140); -// private List samples = new ArrayList<>(60*24); // 1day per default - public FetchSportsSummaryOperation(MiBand2Support support) { super(support); + setName("fetching sport summaries"); } @Override protected void startFetching(TransactionBuilder builder) { + LOG.info("start" + getName()); GregorianCalendar sinceWhen = getLastSuccessfulSyncTime(); builder.write(characteristicFetch, BLETypeConversions.join(new byte[] { MiBand2Service.COMMAND_ACTIVITY_DATA_START_DATE, @@ -80,7 +79,7 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation { @Override protected void handleActivityFetchFinish(boolean success) { - LOG.info("Fetching activity data has finished round " + fetchCount); + LOG.info(getName() + " has finished round " + fetchCount); // GregorianCalendar lastSyncTimestamp = saveSamples(); // if (lastSyncTimestamp != null && needsAnotherFetch(lastSyncTimestamp)) { @@ -137,7 +136,7 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation { */ @Override protected void handleActivityNotif(byte[] value) { - LOG.warn("sports data: " + Logging.formatBytes(value)); + LOG.warn("sports summary data: " + Logging.formatBytes(value)); if (!isOperationRunning()) { LOG.error("ignoring activity data notification because operation is not running. Data length: " + value.length); @@ -155,7 +154,7 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation { lastPacketCounter++; bufferActivityData(value); } else { - GB.toast("Error fetching activity data, invalid package counter: " + value[0] + ", last was: " + lastPacketCounter, Toast.LENGTH_LONG, GB.ERROR); + GB.toast("Error " + getName() + ", invalid package counter: " + value[0] + ", last was: " + lastPacketCounter, Toast.LENGTH_LONG, GB.ERROR); handleActivityFetchFinish(false); return; } diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/Tryout.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/Tryout.java index 17a720b69..9aca15ed7 100644 --- a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/Tryout.java +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/Tryout.java @@ -23,6 +23,11 @@ public class Tryout extends TestBase { byte b = (byte) v; LOG.info("v: " + v); Logging.logBytes(LOG, new byte[] { b }); + + byte[] bs = new byte[] {(byte) 0xf0,0x28,0x00,0x00 }; + LOG.warn("uint32: " + BLETypeConversions.toUint32(bs)); + LOG.warn("uint16: " + BLETypeConversions.toUint16(bs)); + } @Test From c8b71677cde4cf6ac1a36a81abc98ef4f6982e82 Mon Sep 17 00:00:00 2001 From: cpfeiffer Date: Thu, 2 Nov 2017 00:18:48 +0100 Subject: [PATCH 07/17] Bip: remember last time synced --- .../service/devices/amazfitbip/BipActivityType.java | 4 ++-- .../miband2/operations/AbstractFetchOperation.java | 2 +- .../operations/FetchSportsDetailsOperation.java | 11 ++++++++--- .../operations/FetchSportsSummaryOperation.java | 13 ++++--------- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/BipActivityType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/BipActivityType.java index 2a57bc9a8..483e11867 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/BipActivityType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/BipActivityType.java @@ -5,8 +5,8 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; public enum BipActivityType { Outdoor(1), Treadmill(2), - Cycling(3), - Walking(4); + Cycling(3), // should be Walking + Walking(4); // should be cycling private final int code; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/AbstractFetchOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/AbstractFetchOperation.java index 9b28c449e..ca08c2429 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/AbstractFetchOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/AbstractFetchOperation.java @@ -186,7 +186,7 @@ public abstract class AbstractFetchOperation extends AbstractMiBand2Operation { return calendar; } GregorianCalendar calendar = BLETypeConversions.createCalendar(); - calendar.add(Calendar.DAY_OF_MONTH, -10); + calendar.add(Calendar.DAY_OF_MONTH, - 100); return calendar; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsDetailsOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsDetailsOperation.java index 259951212..de555449b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsDetailsOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsDetailsOperation.java @@ -16,6 +16,7 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations; +import android.support.annotation.NonNull; import android.widget.Toast; import org.slf4j.Logger; @@ -51,13 +52,15 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB; public class FetchSportsDetailsOperation extends AbstractFetchOperation { private static final Logger LOG = LoggerFactory.getLogger(FetchSportsDetailsOperation.class); private final BaseActivitySummary summary; + private final String lastSyncTimeKey; private ByteArrayOutputStream buffer; - public FetchSportsDetailsOperation(BaseActivitySummary summary, MiBand2Support support) { + public FetchSportsDetailsOperation(@NonNull BaseActivitySummary summary, @NonNull MiBand2Support support, @NonNull String lastSyncTimeKey) { super(support); setName("fetching sport details"); this.summary = summary; + this.lastSyncTimeKey = lastSyncTimeKey; } @Override @@ -102,6 +105,9 @@ public class FetchSportsDetailsOperation extends AbstractFetchOperation { summary.setGpxTrack(targetFile.getAbsolutePath()); dbHandler.getDaoSession().getBaseActivitySummaryDao().update(summary); } + GregorianCalendar endTime = BLETypeConversions.createCalendar(); + endTime.setTime(summary.getEndTime()); + saveLastSyncTimestamp(endTime); } catch (Exception ex) { GB.toast(getContext(), "Error getting activity details: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex); } @@ -164,11 +170,10 @@ public class FetchSportsDetailsOperation extends AbstractFetchOperation { @Override protected String getLastSyncTimeKey() { - return getDevice().getAddress() + "_" + "lastSportsSummaryTimeMillis"; + return lastSyncTimeKey; } protected GregorianCalendar getLastSuccessfulSyncTime() { - // FIXME: remove this! GregorianCalendar calendar = BLETypeConversions.createCalendar(); calendar.setTime(summary.getStartTime()); return calendar; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsSummaryOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsSummaryOperation.java index d6c4b8a00..ff3709699 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsSummaryOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsSummaryOperation.java @@ -68,6 +68,8 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation { protected void startFetching(TransactionBuilder builder) { LOG.info("start" + getName()); GregorianCalendar sinceWhen = getLastSuccessfulSyncTime(); + builder.notify(characteristicActivityData, true); + builder.notify(characteristicFetch, true); builder.write(characteristicFetch, BLETypeConversions.join(new byte[] { MiBand2Service.COMMAND_ACTIVITY_DATA_START_DATE, AmazfitBipService.COMMAND_ACTIVITY_DATA_TYPE_SPORTS_SUMMARIES}, @@ -109,7 +111,7 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation { super.handleActivityFetchFinish(success); if (summary != null) { - FetchSportsDetailsOperation nextOperation = new FetchSportsDetailsOperation(summary, getSupport()); + FetchSportsDetailsOperation nextOperation = new FetchSportsDetailsOperation(summary, getSupport(), getLastSyncTimeKey()); try { nextOperation.perform(); } catch (IOException ex) { @@ -237,13 +239,6 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation { @Override protected String getLastSyncTimeKey() { - return getDevice().getAddress() + "_" + "lastSportsSummaryTimeMillis"; - } - - protected GregorianCalendar getLastSuccessfulSyncTime() { - // FIXME: remove this! - GregorianCalendar calendar = BLETypeConversions.createCalendar(); - calendar.add(Calendar.DAY_OF_MONTH, -25); - return calendar; + return getDevice().getAddress() + "_" + "lastSportsActivityTimeMillis"; } } From 8b66d0f0f78ce7f427977796ad1922f4a0e29e47 Mon Sep 17 00:00:00 2001 From: cpfeiffer Date: Thu, 2 Nov 2017 00:19:31 +0100 Subject: [PATCH 08/17] Bip: oups, that was just for testing --- .../devices/miband2/operations/FetchSportsSummaryOperation.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsSummaryOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsSummaryOperation.java index ff3709699..60a9c5039 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsSummaryOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsSummaryOperation.java @@ -68,8 +68,6 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation { protected void startFetching(TransactionBuilder builder) { LOG.info("start" + getName()); GregorianCalendar sinceWhen = getLastSuccessfulSyncTime(); - builder.notify(characteristicActivityData, true); - builder.notify(characteristicFetch, true); builder.write(characteristicFetch, BLETypeConversions.join(new byte[] { MiBand2Service.COMMAND_ACTIVITY_DATA_START_DATE, AmazfitBipService.COMMAND_ACTIVITY_DATA_TYPE_SPORTS_SUMMARIES}, From d9de8e0cabc6e6972ff24d1a8c30ad0792cf2958 Mon Sep 17 00:00:00 2001 From: cpfeiffer Date: Fri, 3 Nov 2017 21:54:48 +0100 Subject: [PATCH 09/17] Bip: new activity type mapping --- .../freeyourgadget/gadgetbridge/model/ActivityKind.java | 7 +++++++ .../service/devices/amazfitbip/BipActivityType.java | 6 +++--- app/src/main/res/values/strings.xml | 1 + 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityKind.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityKind.java index fc610aa29..2a42f93f9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityKind.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityKind.java @@ -68,6 +68,9 @@ public class ActivityKind { if ((types & ActivityKind.TYPE_CYCLING) != 0) { result[i++] = provider.toRawActivityKind(TYPE_CYCLING); } + if ((types & ActivityKind.TYPE_TREADMILL) != 0) { + result[i++] = provider.toRawActivityKind(TYPE_TREADMILL); + } return Arrays.copyOf(result, i); } @@ -91,6 +94,8 @@ public class ActivityKind { return context.getString(R.string.activity_type_swimming); case TYPE_CYCLING: return context.getString(R.string.activity_type_biking); + case TYPE_TREADMILL: + return context.getString(R.string.activity_type_treadmill); case TYPE_UNKNOWN: default: return context.getString(R.string.activity_type_unknown); @@ -112,6 +117,8 @@ public class ActivityKind { return R.drawable.ic_activity_walking; case TYPE_CYCLING: return R.drawable.ic_activity_biking; + case TYPE_TREADMILL: + return R.drawable.ic_activity_walking; case TYPE_SWIMMING: // fall through case TYPE_NOT_WORN: // fall through case TYPE_ACTIVITY: // fall through diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/BipActivityType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/BipActivityType.java index 483e11867..eef7a14e6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/BipActivityType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/BipActivityType.java @@ -3,10 +3,10 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.amazfitbip; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; public enum BipActivityType { - Outdoor(1), + Outdoor(4), Treadmill(2), - Cycling(3), // should be Walking - Walking(4); // should be cycling + Cycling(1), + Walking(3); private final int code; diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6ed0638ee..cbc1db1bb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -496,4 +496,5 @@ Unknown activity Activities Biking + Treadmill From 9e79eee70763273dc98afd5f7554695a44315ff5 Mon Sep 17 00:00:00 2001 From: cpfeiffer Date: Fri, 3 Nov 2017 22:04:01 +0100 Subject: [PATCH 10/17] Bip wip: fix position -> item mapping --- .../gadgetbridge/activities/ActivitySummariesActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesActivity.java index 721251e81..4ddea15c1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesActivity.java @@ -26,7 +26,7 @@ public class ActivitySummariesActivity extends AbstractListActivity parent, View view, int position, long id) { - Object item = getItemAdapter().getItem(position); + Object item = parent.getItemAtPosition(position); if (item != null) { ActivitySummary summary = (ActivitySummary) item; String gpxTrack = summary.getGpxTrack(); From ac981115027548c02fd41edced0cbd99867a29f9 Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Sun, 5 Nov 2017 00:08:39 +0100 Subject: [PATCH 11/17] Amazfit Bip: Fix Running/Cycling activity mapping (was swapped) --- .../service/devices/amazfitbip/BipActivityType.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/BipActivityType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/BipActivityType.java index eef7a14e6..bc842f357 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/BipActivityType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/BipActivityType.java @@ -3,10 +3,10 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.amazfitbip; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; public enum BipActivityType { - Outdoor(4), + Outdoor(1), Treadmill(2), - Cycling(1), - Walking(3); + Walking(3), + Cycling(4); private final int code; From 24f98504b0504dd9d1d522220e0eeebc7f545de9 Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Sun, 5 Nov 2017 00:20:26 +0100 Subject: [PATCH 12/17] Amazfit Bip: when deleting activites, delete the correct one --- .../gadgetbridge/activities/ActivitySummariesActivity.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesActivity.java index 4ddea15c1..209544e83 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesActivity.java @@ -18,6 +18,8 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB; public class ActivitySummariesActivity extends AbstractListActivity { + private int selectedIndex; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -45,8 +47,7 @@ public class ActivitySummariesActivity extends AbstractListActivity parent, View view, int position, long id) { + selectedIndex = position; return getItemListView().showContextMenu(); } }); From a58e3f66ce2320d2a850af5e3579d3ef4a0ef989 Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Sun, 5 Nov 2017 22:35:28 +0100 Subject: [PATCH 13/17] try to parse timezone that comes back from huami devices (there are other places where something like this has to be done, probably also in the other direction) related to #869 --- .../gadgetbridge/service/btle/BLETypeConversions.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java index d09e8ad51..71dff949c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java @@ -147,6 +147,12 @@ public class BLETypeConversions { value[6] & 0xff ); + if (value.length > 7) { + TimeZone timeZone = TimeZone.getDefault(); + timeZone.setRawOffset(value[7] * 15 * 60 * 1000); + timestamp.setTimeZone(timeZone); + } + if (honorDeviceTimeOffset) { int offsetInHours = MiBandCoordinator.getDeviceTimeOffsetHours(); if (offsetInHours != 0) { From 99710e7db39ad1372975a27bc63b09c6b3dca272 Mon Sep 17 00:00:00 2001 From: cpfeiffer Date: Mon, 6 Nov 2017 23:21:04 +0100 Subject: [PATCH 14/17] Tiny test for parsing date,time,tz --- .../test/BLETypeConversionsTest.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/BLETypeConversionsTest.java diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/BLETypeConversionsTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/BLETypeConversionsTest.java new file mode 100644 index 000000000..5360a6516 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/BLETypeConversionsTest.java @@ -0,0 +1,25 @@ +package nodomain.freeyourgadget.gadgetbridge.test; + +import org.junit.Test; + +import java.util.GregorianCalendar; + +import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; + +import static org.junit.Assert.assertTrue; + +public class BLETypeConversionsTest extends TestBase { + @Test + public void testTimeParsing() { + byte[] requested = new byte[] { + (byte) 0xe1, 0x07, 0x0a, 0x1d, 0x00, 0x1c, 0x00,0x08 + }; + byte[] received = new byte[] { + (byte) 0xe1, 0x07, 0x0a, 0x1c, 0x17, 0x1c, 0x00, 0x04 + }; + GregorianCalendar calRequested = BLETypeConversions.rawBytesToCalendar(requested, false); + GregorianCalendar calReceived = BLETypeConversions.rawBytesToCalendar(received, false); + + assertTrue(calRequested.getTime().equals(calReceived.getTime())); + } +} From b76e78768c41c08d90f8314795bf7f14084c7c3a Mon Sep 17 00:00:00 2001 From: cpfeiffer Date: Tue, 7 Nov 2017 21:23:54 +0100 Subject: [PATCH 15/17] Another small tz+dst parsing testcase --- .../test/BLETypeConversionsTest.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/BLETypeConversionsTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/BLETypeConversionsTest.java index 5360a6516..70c39e352 100644 --- a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/BLETypeConversionsTest.java +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/BLETypeConversionsTest.java @@ -10,7 +10,7 @@ import static org.junit.Assert.assertTrue; public class BLETypeConversionsTest extends TestBase { @Test - public void testTimeParsing() { + public void testTimeParsing1() { byte[] requested = new byte[] { (byte) 0xe1, 0x07, 0x0a, 0x1d, 0x00, 0x1c, 0x00,0x08 }; @@ -22,4 +22,18 @@ public class BLETypeConversionsTest extends TestBase { assertTrue(calRequested.getTime().equals(calReceived.getTime())); } + + @Test + public void testTimeParsingWithDST() { + byte[] requested = new byte[] { + (byte) 0xe1,0x07,0x0a,0x09,0x11,0x23,0x00,0x08 + }; + byte[] received = new byte[] { + (byte) 0xe1,0x07,0x0a,0x09,0x10,0x23,0x00,0x04 + }; + GregorianCalendar calRequested = BLETypeConversions.rawBytesToCalendar(requested, false); + GregorianCalendar calReceived = BLETypeConversions.rawBytesToCalendar(received, false); + + assertTrue(calRequested.getTime().equals(calReceived.getTime())); + } } From da7de378d390e0ea8f70181aefffcb40df4b6976 Mon Sep 17 00:00:00 2001 From: cpfeiffer Date: Tue, 7 Nov 2017 21:24:48 +0100 Subject: [PATCH 16/17] Back out the DST handling, since it causes problems with activity fetching Details: when we ask to fetch activity samples from date:time:tz+dst, the band, under certain conditions, will send us back date:time:tz (without the dst offset) We're fine with that, so we start fetching. When it's done, we take the last sample's timestamp (still without dst offset), convert it to a unix timestamp, create a Calendar using current tz and apply the unix timestamp. Then we send that timestamp again to the band in order to fetch activity samples from then, but we again add the dst offset to the tz, so send as date:time:tz+dst without changing the timestamp. That way, we may end up at the timestamp we began with, fetching the same activity data again and not progressing. We first need to thorougly understand how the devices behave, before we can reenable and fix this. --- .../gadgetbridge/service/btle/BLETypeConversions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java index 71dff949c..47b46a172 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java @@ -265,7 +265,7 @@ public class BLETypeConversions { */ public static byte mapTimeZone(TimeZone timeZone, int timezoneFlags) { int offsetMillis = timeZone.getRawOffset(); - if (timezoneFlags == TZ_FLAG_INCLUDE_DST_IN_TZ) { + if (false && timezoneFlags == TZ_FLAG_INCLUDE_DST_IN_TZ) { offsetMillis += timeZone.getDSTSavings(); } int utcOffsetInHours = (offsetMillis / (1000 * 60 * 60)); From a8fdcb50f3a26437525c58ca7245df796243c886 Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Tue, 13 Mar 2018 21:17:14 +0100 Subject: [PATCH 17/17] fix bad merge from master --- .../gadgetbridge/activities/charts/AbstractChartFragment.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java index fea3a3277..e9fa8f020 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java @@ -70,6 +70,8 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; +import static nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils.isValidHeartRateValue; + /** * A base class fragment to be used with ChartsActivity. The fragment can supply * a title to be displayed in the activity by returning non-null in #getTitle() @@ -510,7 +512,7 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { } activityEntries.add(createLineEntry(value, ts)); } - if (hr && sample.getKind() != ActivityKind.TYPE_NOT_WORN && isValidHeartRateValue(sample.getHeartRate())) { + if (hr && sample.getKind() != ActivityKind.TYPE_NOT_WORN && HeartRateUtils.isValidHeartRateValue(sample.getHeartRate())) { if (lastHrSampleIndex > -1 && ts - lastHrSampleIndex > 1800*HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) { heartrateEntries.add(createLineEntry(0, lastHrSampleIndex + 1)); heartrateEntries.add(createLineEntry(0, ts - 1));