diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 9b19e7e96d..1afb4246d9 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"; @@ -42,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); @@ -69,6 +72,8 @@ public static void main(String[] args) throws Exception { addCalendarSyncState(schema, device); + addBipActivitySummary(schema, user, device); + new DaoGenerator().generateAll(schema, "app/src/main/java"); } @@ -309,6 +314,31 @@ private static void addCalendarSyncState(Schema schema, Entity device) { 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 ac716aaf84..c1bad1947a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -59,6 +59,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 0a9742ecf4..7370690c12 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java @@ -25,6 +25,9 @@ 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; @@ -607,4 +610,24 @@ public static GBApplication app() { 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 302d7babe6..725533574e 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 static String formatBytes(byte[] bytes) { } 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 0000000000..bbdd11e2a2 --- /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 0000000000..209544e835 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesActivity.java @@ -0,0 +1,82 @@ +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 { + + private int selectedIndex; + + @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 = parent.getItemAtPosition(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) { + deleteItemAt(selectedIndex); + return true; + } + }); + } + }); + + getItemListView().setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { + @Override + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + selectedIndex = position; + 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/activities/HeartRateUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/HeartRateUtils.java index 1b2730f60c..a2f3b0734c 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 b61513847b..e9fa8f0207 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.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() @@ -439,6 +441,7 @@ protected DefaultChartsData refresh(GBDevice gbDevice, List notWornEntries = new ArrayList<>(numEntries); boolean hr = supportsHeartrate(gbDevice); List heartrateEntries = hr ? new ArrayList(numEntries) : null; + List colors = new ArrayList<>(numEntries); // this is kinda inefficient... int lastHrSampleIndex = -1; for (int i = 0; i < numEntries; i++) { @@ -509,7 +512,7 @@ protected DefaultChartsData refresh(GBDevice gbDevice, List -1 && ts - lastHrSampleIndex > 1800*HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) { heartrateEntries.add(createLineEntry(0, lastHrSampleIndex + 1)); heartrateEntries.add(createLineEntry(0, ts - 1)); @@ -574,10 +577,6 @@ protected DefaultChartsData refresh(GBDevice gbDevice, List 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 67b86c5fb3..53c7563fd1 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 void onReceive(Context context, Intent intent) { 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(); 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 0000000000..9ad6e57a18 --- /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 0000000000..c90ff1831a --- /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 105a1316bd..84d7ea6f70 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 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 void onClick(View v) { } ); + //show activity tracks + holder.showActivityTracks.setVisibility(coordinator.supportsActivityTracks() ? View.VISIBLE : View.GONE); + holder.showActivityTracks.setOnClickListener(new View.OnClickListener() + { + @Override + public void onClick(View v) { + Intent startIntent; + startIntent = new Intent(context, ActivitySummariesActivity.class); + startIntent.putExtra(GBDevice.EXTRA_DEVICE, device); + context.startActivity(startIntent); + } + } + ); + ItemWithDetailsAdapter infoAdapter = new ItemWithDetailsAdapter(context, device.getDeviceInfos()); infoAdapter.setHorizontalAlignment(true); holder.deviceInfoList.setAdapter(infoAdapter); @@ -338,6 +353,7 @@ static class ViewHolder extends RecyclerView.ViewHolder { ImageView manageAppsView; ImageView setAlarmsView; ImageView showActivityGraphs; + ImageView showActivityTracks; ImageView deviceInfoView; //overflow @@ -365,6 +381,7 @@ static class ViewHolder extends RecyclerView.ViewHolder { manageAppsView = (ImageView) view.findViewById(R.id.device_action_manage_apps); setAlarmsView = (ImageView) view.findViewById(R.id.device_action_set_alarms); showActivityGraphs = (ImageView) view.findViewById(R.id.device_action_show_activity_graphs); + showActivityTracks = (ImageView) view.findViewById(R.id.device_action_show_activity_tracks); deviceInfoView = (ImageView) view.findViewById(R.id.device_info_image); deviceInfoBox = (RelativeLayout) view.findViewById(R.id.device_item_infos_box); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/ItemWithDetailsAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/ItemWithDetailsAdapter.java index 962a894c58..f038655d20 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/ItemWithDetailsAdapter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/ItemWithDetailsAdapter.java @@ -17,77 +17,33 @@ package nodomain.freeyourgadget.gadgetbridge.adapter; import android.content.Context; -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.List; -import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails; /** * Adapter for displaying generic ItemWithDetails instances. */ -public class ItemWithDetailsAdapter 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 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; + super(context, items); } - public void setHorizontalAlignment(boolean horizontalAlignment) { - this.horizontalAlignment = horizontalAlignment; + @Override + protected String getName(ItemWithDetails item) { + return item.getName(); } @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 getDetails(ItemWithDetails item) { + return item.getDetails(); } - public void setSize(int size) { - this.size = size; + @Override + protected int getIcon(ItemWithDetails item) { + return item.getIcon(); } - public int getSize() { - return size; - } } 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 1f9c13d999..0000000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/UsedConfiguration.java +++ /dev/null @@ -1,32 +0,0 @@ -/* Copyright (C) 2015-2018 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 194d642e45..6acbe6e4cf 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 boolean isHealthWearable(BluetoothDevice device) { 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 54d99a334f..45d4150ebd 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...) 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 0000000000..baf58485a1 --- /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 0ea400c075..25a1f1db03 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 @@ -64,6 +64,11 @@ public boolean supportsHeartRateMeasurement(GBDevice device) { return true; } + @Override + public boolean supportsActivityTracks() { + return true; + } + @Override public boolean supportsWeather() { 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 0c439bf04d..38b81f3f84 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 @@ -36,5 +36,8 @@ public class AmazfitBipService { public static final byte[] COMMAND_SET_LANGUAGE_NEW_TEMPLATE = new byte[]{ENDPOINT_DISPLAY, 0x17, 0x00, 0, 0, 0, 0, 0}; + public static final byte COMMAND_ACTIVITY_DATA_TYPE_SPORTS_SUMMARIES = 0x05; + public static final byte COMMAND_ACTIVITY_DATA_TYPE_SPORTS_DETAILS = 0x06; + public static final byte[] COMMAND_ACK_FIND_PHONE_IN_PROGRESS = new byte[]{ENDPOINT_DISPLAY, 0x14, 0x00, 0x00}; } 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 1b12910fc1..0dac10166b 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,38 +20,15 @@ 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 { - - // 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); } @@ -111,39 +88,11 @@ private int determinePreviousValidActivityType(MiBandActivitySample sample) { @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 0000000000..a153dda97e --- /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_CYCLING; + 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 0000000000..914fcc666a --- /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 0000000000..5ae0ba7468 --- /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 6dc1eb3d77..774bde62d4 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,6 +32,11 @@ 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_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; @@ -47,7 +56,75 @@ public static int[] mapToDBActivityTypes(int types, SampleProvider provider) { 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_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); } + 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_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); + } + } + + @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_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 + 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 0000000000..da0c6d2c42 --- /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 0000000000..9bb670a1f6 --- /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 0000000000..22507c312f --- /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 0000000000..e8b51d76ca --- /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/AbstractBTLEOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEOperation.java index e640952412..5dc29645ff 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 @@ 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 @@ protected GBDevice getDevice() { 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/BLETypeConversions.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java index 49e05c31ae..55ecb9dcc4 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 @@ -30,6 +30,9 @@ * 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 * @@ -164,10 +167,33 @@ public static GregorianCalendar rawBytesToCalendar(byte[] value, boolean honorDe 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), @@ -230,7 +256,20 @@ public static byte[] calendarToLocalTimeBytes(GregorianCalendar now) { * @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 (false && 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/BTLEOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BTLEOperation.java index 973a795fcd..779e9f5a48 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 @@ 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/btle/BtLEQueue.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java index 80509afcc3..1f2cb4d348 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.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 void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descri 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 0000000000..ae1ef5d137 --- /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 189ffdd4de..29c3eb31a1 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 @@ -49,10 +49,10 @@ 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; @@ -285,7 +285,8 @@ public void onSendWeather(WeatherSpec weatherSpec) { @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/BipActivityType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/BipActivityType.java new file mode 100644 index 0000000000..bc842f3574 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/BipActivityType.java @@ -0,0 +1,53 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.amazfitbip; + +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; + +public enum BipActivityType { + Outdoor(1), + Treadmill(2), + Walking(3), + Cycling(4); + + private final int code; + + BipActivityType(final int code) { + this.code = code; + } + + 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 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: + return Treadmill; + case ActivityKind.TYPE_CYCLING: + return Cycling; + case ActivityKind.TYPE_WALKING: + return Walking; + } + throw new RuntimeException("No matching activity activityKind: " + activityKind); + } +} 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 ce9b98f9e1..61fcd0ecae 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.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 @@ -89,8 +89,8 @@ protected String getLastSyncTimeKey() { } @Override - protected void handleActivityFetchFinish() { - LOG.info("Fetching log data has finished"); + protected void handleActivityFetchFinish(boolean success) { + LOG.info(getName() +" data has finished"); try { logOutputStream.close(); logOutputStream = null; @@ -98,7 +98,7 @@ protected void handleActivityFetchFinish() { LOG.warn("could not close output stream", e); return; } - super.handleActivityFetchFinish(); + super.handleActivityFetchFinish(success); } @Override @@ -113,18 +113,18 @@ protected void handleActivityNotif(byte[] value) { lastPacketCounter++; bufferActivityData(value); } else { - GB.toast("Error fetching activity data, invalid package counter: " + value[0], Toast.LENGTH_LONG, GB.ERROR); - handleActivityFetchFinish(); + GB.toast("Error " + getName() + " invalid package counter: " + value[0], Toast.LENGTH_LONG, GB.ERROR); + 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 fefeba9e49..735924f2f6 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.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; @@ -69,6 +70,7 @@ 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; @@ -105,6 +107,7 @@ 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; @@ -214,6 +217,14 @@ protected TransactionBuilder initializeDevice(TransactionBuilder builder) { 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) { @@ -223,7 +234,8 @@ public byte[] getTimeBytes(Calendar calendar, TimeUnit precision) { } 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; @@ -1189,7 +1201,7 @@ public void doCurrentSample() { 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); @@ -1372,10 +1384,9 @@ public void onSendConfiguration(String config) { @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 a63c17b350..dcad7583ee 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 AbstractFetchOperation(MiBand2Support support) { 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 @@ protected void doPerform() throws IOException { 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())); @@ -115,7 +117,7 @@ public boolean onCharacteristicChanged(BluetoothGatt gatt, } @CallSuper - protected void handleActivityFetchFinish() { + protected void handleActivityFetchFinish(boolean success) { operationFinished(); unsetBusy(); } @@ -139,7 +141,8 @@ protected void handleActivityMetadata(byte[] value) { // 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)); @@ -149,18 +152,18 @@ protected void handleActivityMetadata(byte[] value) { 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); } } @@ -183,7 +186,7 @@ protected GregorianCalendar getLastSuccessfulSyncTime() { 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/FetchActivityOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchActivityOperation.java index b25b75766b..2d11799e48 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 @@ -74,24 +75,24 @@ protected void startFetching(TransactionBuilder builder) { builder.write(characteristicFetch, new byte[] { MiBand2Service.COMMAND_FETCH_DATA}); } - protected void handleActivityFetchFinish() { - LOG.info("Fetching activity data has finished round " + fetchCount); + protected void handleActivityFetchFinish(boolean success) { + 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); } } - super.handleActivityFetchFinish(); + super.handleActivityFetchFinish(success); } 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,13 @@ protected void handleActivityNotif(byte[] value) { lastPacketCounter++; bufferActivityData(value); } else { - GB.toast("Error fetching activity data, invalid package counter: " + value[0], Toast.LENGTH_LONG, GB.ERROR); - handleActivityFetchFinish(); + 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 new file mode 100644 index 0000000000..de555449bf --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchSportsDetailsOperation.java @@ -0,0 +1,181 @@ +/* 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.support.annotation.NonNull; +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 final String lastSyncTimeKey; + + private ByteArrayOutputStream buffer; + + public FetchSportsDetailsOperation(@NonNull BaseActivitySummary summary, @NonNull MiBand2Support support, @NonNull String lastSyncTimeKey) { + super(support); + setName("fetching sport details"); + this.summary = summary; + this.lastSyncTimeKey = lastSyncTimeKey; + } + + @Override + protected void startFetching(TransactionBuilder builder) { + LOG.info("start " + getName()); + 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(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); +// } +// } + + + 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); + } + 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); + } + } + + 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 details: " + Logging.formatBytes(value)); + + if (!isOperationRunning()) { + 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 details data length: " + value.length); + getSupport().logMessageContent(value); + return; + } + + if ((byte) (lastPacketCounter + 1) == value[0] ) { + lastPacketCounter++; + bufferActivityData(value); + } else { + GB.toast("Error " + getName() + ", 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 lastSyncTimeKey; + } + + protected GregorianCalendar getLastSuccessfulSyncTime() { + 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 2441f6f11d..ffa69729d0 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 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.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.amazfitbip.BipActivityType; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.MiBand2Support; import nodomain.freeyourgadget.gadgetbridge.util.GB; @@ -39,29 +57,30 @@ 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 byte lastPacketCounter; + private ByteArrayOutputStream buffer = new ByteArrayOutputStream(140); 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, -// 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() { - LOG.info("Fetching activity data has finished round " + fetchCount); + protected void handleActivityFetchFinish(boolean success) { + LOG.info(getName() + " has finished round " + fetchCount); + // GregorianCalendar lastSyncTimestamp = saveSamples(); // if (lastSyncTimestamp != null && needsAnotherFetch(lastSyncTimestamp)) { // try { @@ -72,7 +91,31 @@ protected void handleActivityFetchFinish() { // } // } - 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(), getLastSyncTimeKey()); + try { + nextOperation.perform(); + } catch (IOException ex) { + GB.toast(getContext(), "Unable to fetch activity details: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex); + } + } } @Override @@ -93,7 +136,7 @@ public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteris */ @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); @@ -101,48 +144,99 @@ protected void handleActivityNotif(byte[] value) { 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 " + getName() + ", 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 activityKind = ActivityKind.TYPE_UNKNOWN; + try { + int rawKind = BLETypeConversions.toUnsigned(buffer.getShort()); + BipActivityType activityType = BipActivityType.fromCode(rawKind); + activityKind = activityType.toActivityKind(); + } catch (Exception ex) { + LOG.error("Error mapping acivity kind: " + ex.getMessage(), ex); + } + 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"; - } - - - protected GregorianCalendar getLastSuccessfulSyncTime() { - // FIXME: remove this! - GregorianCalendar calendar = BLETypeConversions.createCalendar(); - calendar.add(Calendar.DAY_OF_MONTH, -1); - return calendar; + return getDevice().getAddress() + "_" + "lastSportsActivityTimeMillis"; } } 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 ccbf155530..9c5afcb0dd 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/AndroidUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/AndroidUtils.java @@ -21,6 +21,7 @@ import android.content.BroadcastReceiver; import android.content.ContentUris; import android.content.Context; +import android.content.Intent; import android.content.res.Configuration; import android.database.Cursor; import android.graphics.Color; @@ -33,9 +34,12 @@ import android.provider.MediaStore; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.content.FileProvider; import android.support.v4.content.LocalBroadcastManager; import android.text.TextUtils; +import java.io.File; +import java.io.IOException; import java.net.URISyntaxException; import java.util.Locale; @@ -43,6 +47,12 @@ import nodomain.freeyourgadget.gadgetbridge.R; 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; @@ -207,4 +217,15 @@ private static String colorToHex(int color) { } throw new IllegalArgumentException("Unable to decode the given uri to a file path: " + uri); } + + 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 2adab36c5d..7a43250d19 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 @@ 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 ccf2d397a6..5fb11e3c34 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/FileUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/FileUtils.java @@ -310,4 +310,14 @@ public static File createTempDir(String prefix) throws IOException { } 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 0000000000..7001d100b9 --- /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 0000000000..abdaea833d --- /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 0000000000..abdaea833d --- /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 0000000000..7a6bb2d018 --- /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 0000000000..14c06266a8 --- /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 0000000000..14c06266a8 --- /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 0000000000..4e323aeaa5 --- /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 0000000000..831c0b3456 --- /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 0000000000..170019522d --- /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 517cd04446..09fcb4e20c 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" /> + + Mute Reply + Your activity tracks + Not measured + Activity + Light sleep + Deep sleep + Device not worn + Running + Walking + Swimming + Unknown activity + Activities + Biking + Treadmill + Firmware Invalid data Font 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 ffa74ab562..2d4b2d4c1d 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 0000000000..6b9fa25f24 --- /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/BLETypeConversionsTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/BLETypeConversionsTest.java new file mode 100644 index 0000000000..70c39e3521 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/BLETypeConversionsTest.java @@ -0,0 +1,39 @@ +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 testTimeParsing1() { + 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())); + } + + @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())); + } +} 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 0000000000..da22ee0eb2 --- /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 0000000000..a4b5f5b954 --- /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/java/nodomain/freeyourgadget/gadgetbridge/test/Tryout.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/Tryout.java index 17a720b697..9aca15ed71 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 void blah() { 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 diff --git a/app/src/test/resources/ActivityDetailsDump1.txt b/app/src/test/resources/ActivityDetailsDump1.txt new file mode 100644 index 0000000000..f5c33455ad --- /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