diff --git a/app/build.gradle b/app/build.gradle
index cc5d972947..d811cb97fc 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -130,10 +130,10 @@ task pmd(type: Pmd) {
xml.enabled = false
html.enabled = true
xml {
- destination "$project.buildDir/reports/pmd/pmd.xml"
+ destination new File("$project.buildDir/reports/pmd/pmd.xml")
}
html {
- destination "$project.buildDir/reports/pmd/pmd.html"
+ destination new File("$project.buildDir/reports/pmd/pmd.html")
}
}
}
@@ -150,10 +150,10 @@ task findbugs(type: FindBugs) {
xml.enabled = false
html.enabled = true
xml {
- destination "$project.buildDir/reports/findbugs/findbugs-output.xml"
+ destination new File("$project.buildDir/reports/findbugs/findbugs-output.xml")
}
html {
- destination "$project.buildDir/reports/findbugs/findbugs-output.html"
+ destination new File("$project.buildDir/reports/findbugs/findbugs-output.html")
}
}
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index bf3ae12fb1..af64536dcd 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -34,6 +34,8 @@
android:name="android.hardware.telephony"
android:required="false" />
+
+
+
+
. */
+package nodomain.freeyourgadget.gadgetbridge.contentprovider;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.content.LocalBroadcastManager;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
+
+/**
+ * A content Provider, which publishes read only RAW @see ActivitySample to other applications
+ *
+ */
+public class HRContentProvider extends ContentProvider {
+ private static final Logger LOG = LoggerFactory.getLogger(HRContentProvider.class);
+
+ private static final int DEVICES_LIST = 1;
+ private static final int REALTIME = 2;
+ private static final int ACTIVITY_START = 3;
+ private static final int ACTIVITY_STOP = 4;
+
+ enum provider_state {ACTIVE, CONNECTING, INACTIVE};
+ provider_state state = provider_state.INACTIVE;
+
+ private static final UriMatcher URI_MATCHER;
+ private Timer punchTimer = new Timer();
+
+ static {
+ URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
+ URI_MATCHER.addURI(HRContentProviderContract.AUTHORITY,
+ "devices", DEVICES_LIST);
+ URI_MATCHER.addURI(HRContentProviderContract.AUTHORITY,
+ "realtime", REALTIME);
+ URI_MATCHER.addURI(HRContentProviderContract.AUTHORITY,
+ "activity_start", ACTIVITY_START);
+ URI_MATCHER.addURI(HRContentProviderContract.AUTHORITY,
+ "activity_stop", ACTIVITY_STOP);
+ }
+
+ private ActivitySample buffered_sample = null;
+
+ private GBDevice mGBDevice = null;
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ //LOG.info("Received Event, aciton: " + action);
+
+ switch (action) {
+ case GBDevice.ACTION_DEVICE_CHANGED:
+ mGBDevice = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
+ LOG.debug("ACTION DEVICE CHANGED Got Device " + mGBDevice);
+
+ // Rationale: If device was not connected
+ // it should show up here after being connected
+ // If the user wanted to switch on realtime traffic, but we first needed to connect it
+ // we do it here
+ if (mGBDevice.isConnected() && state == provider_state.CONNECTING) {
+ LOG.debug("Device connected now, enabling realtime " + mGBDevice);
+
+ enableContinuousRealtimeHeartRateMeasurement();
+ }
+ break;
+ case DeviceService.ACTION_REALTIME_SAMPLES:
+ ActivitySample tmp_sample = (ActivitySample) intent.getSerializableExtra(DeviceService.EXTRA_REALTIME_SAMPLE);
+ //LOG.debug("Got new Sample " + tmp_sample.getHeartRate());
+ if (tmp_sample.getHeartRate() == -1)
+ break;
+
+ buffered_sample = tmp_sample;
+ // This notifies the observer
+ getContext().
+ getContentResolver().
+ notifyChange(Uri.parse(HRContentProviderContract.REALTIME_URL), null);
+ break;
+ default:
+ break;
+ }
+
+ }
+ };
+
+ @Override
+ public boolean onCreate() {
+ LOG.info("Creating...");
+ IntentFilter filterLocal = new IntentFilter();
+
+ filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
+ filterLocal.addAction(DeviceService.ACTION_REALTIME_SAMPLES);
+
+ LocalBroadcastManager.getInstance(this.getContext()).registerReceiver(mReceiver, filterLocal);
+
+ return true;
+ }
+
+ @Override
+ public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+ //LOG.info("query uri " + uri.toString());
+ MatrixCursor mc;
+
+ switch (URI_MATCHER.match(uri)) {
+ case DEVICES_LIST:
+ LOG.info("Get DEVICES LIST");
+ return getDevicesList();
+
+ case ACTIVITY_START:
+ LOG.info("Get ACTIVTY START");
+ return startRealtimeSampling(projection, selectionArgs);
+
+ case ACTIVITY_STOP:
+ LOG.info("Get ACTIVITY STOP");
+ return stopRealtimeSampling(projection, selectionArgs);
+
+ case REALTIME:
+ LOG.info("REALTIME");
+ return getRealtimeSample(projection, selectionArgs);
+ }
+ return null;
+ }
+
+ @Nullable
+ private Cursor getDevicesList() {
+ MatrixCursor mc;
+ DeviceManager deviceManager = ((GBApplication) (this.getContext())).getDeviceManager();
+ List l = deviceManager.getDevices();
+ if (l == null) {
+ return null;
+ }
+ LOG.info(String.format("listing %d devices", l.size()));
+
+ mc = new MatrixCursor(HRContentProviderContract.deviceColumnNames);
+ for (GBDevice dev : l) {
+ mc.addRow(new Object[]{dev.getName(), dev.getModel(), dev.getAddress()});
+ }
+ return mc;
+ }
+
+ @NonNull
+ private Cursor startRealtimeSampling(String[] projection, String[] args) {
+ MatrixCursor mc;
+ this.state = provider_state.CONNECTING;
+
+ GBDevice targetDevice = getDevice((args != null) ? args[0] : "");
+ if (targetDevice != null && targetDevice.isConnected()) {
+ enableContinuousRealtimeHeartRateMeasurement();
+ mc = new MatrixCursor(HRContentProviderContract.activityColumnNames);
+ mc.addRow(new String[]{"OK", "Connected"});
+ } else {
+ GBApplication.deviceService().connect(targetDevice);
+ mc = new MatrixCursor(HRContentProviderContract.activityColumnNames);
+ mc.addRow(new String[]{"OK", "Connecting"});
+ }
+
+ return mc;
+ }
+
+ @NonNull
+ private Cursor stopRealtimeSampling(String[] projection, String[] args) {
+ MatrixCursor mc;
+ this.state = provider_state.INACTIVE;
+
+ GBApplication.deviceService().onEnableRealtimeSteps(false);
+ GBApplication.deviceService().onEnableRealtimeHeartRateMeasurement(false);
+ mc = new MatrixCursor(HRContentProviderContract.activityColumnNames);
+ mc.addRow(new String[]{"OK", "No error"});
+ punchTimer.cancel();
+ punchTimer = new Timer();
+ return mc;
+ }
+
+
+ private void enableContinuousRealtimeHeartRateMeasurement() {
+ this.state = provider_state.ACTIVE;
+ GBApplication.deviceService().onEnableRealtimeSteps(true);
+ GBApplication.deviceService().onEnableRealtimeHeartRateMeasurement(true);
+
+ punchTimer.scheduleAtFixedRate(new TimerTask() {
+ @Override
+ public void run() {
+ LOG.debug("punching the deviceService...");
+ // As seen in LiveActivityFragment:
+ // have to enable it again and again to keep it measureing
+ GBApplication.deviceService().onEnableRealtimeHeartRateMeasurement(true);
+ }
+
+ }, 1000 * 10, 1000);
+ // Start after 10 seconds, repeat each second
+ }
+
+ @NonNull
+ private Cursor getRealtimeSample(String[] projection, String[] args) {
+ MatrixCursor mc;
+ mc = new MatrixCursor(HRContentProviderContract.realtimeColumnNames);
+ if (buffered_sample != null)
+ mc.addRow(new Object[]{"OK", buffered_sample.getHeartRate(), buffered_sample.getSteps(), mGBDevice != null ? mGBDevice.getBatteryLevel() : 99});
+ return mc;
+ }
+
+ // Returns the requested device. If it is not found
+ // it tries to return the "current" device (if i understand it correctly)
+ @Nullable
+ private GBDevice getDevice(String deviceAddress) {
+ DeviceManager deviceManager;
+
+ if (mGBDevice != null && mGBDevice.getAddress().equals(deviceAddress)) {
+ LOG.info(String.format("Found device mGBDevice %s", mGBDevice));
+
+ return mGBDevice;
+ }
+
+ deviceManager = ((GBApplication) (this.getContext())).getDeviceManager();
+ for (GBDevice device : deviceManager.getDevices()) {
+ if (deviceAddress.equals(device.getAddress())) {
+ LOG.info(String.format("Found device device %s", device));
+ return device;
+ }
+ }
+ LOG.info(String.format("Did not find device returning selected %s", deviceManager.getSelectedDevice()));
+ return deviceManager.getSelectedDevice();
+ }
+
+ @Override
+ public String getType(@NonNull Uri uri) {
+ LOG.error("getType uri " + uri);
+ return null;
+ }
+
+ @Override
+ public Uri insert(@NonNull Uri uri, ContentValues values) {
+ return null;
+ }
+
+ @Override
+ public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public int update(@NonNull Uri uri, ContentValues values, String selection, String[]
+ selectionArgs) {
+ return 0;
+ }
+
+ // Das ist eine debugging funktion
+ @Override
+ public void shutdown() {
+ LocalBroadcastManager.getInstance(this.getContext()).unregisterReceiver(mReceiver);
+ super.shutdown();
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/contentprovider/HRContentProviderContract.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/contentprovider/HRContentProviderContract.java
new file mode 100644
index 0000000000..f1a93328cc
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/contentprovider/HRContentProviderContract.java
@@ -0,0 +1,41 @@
+/* Copyright (C) 2018 Benedikt Elser
+
+ 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.contentprovider;
+
+public final class HRContentProviderContract {
+
+ public static final String COLUMN_STATUS = "Status";
+ public static final String COLUMN_NAME = "Name";
+ public static final String COLUMN_ADDRESS = "Address";
+ public static final String COLUMN_MODEL = "Model";
+ public static final String COLUMN_MESSAGE = "Message";
+ public static final String COLUMN_HEARTRATE = "HeartRate";
+ public static final String COLUMN_STEPS = "Steps";
+ public static final String COLUMN_BATTERY = "Battery";
+
+ public static final String[] deviceColumnNames = new String[]{COLUMN_NAME, COLUMN_MODEL, COLUMN_ADDRESS};
+ public static final String[] activityColumnNames = new String[]{COLUMN_STATUS, COLUMN_MESSAGE};
+ public static final String[] realtimeColumnNames = new String[]{COLUMN_STATUS, COLUMN_HEARTRATE, COLUMN_STEPS, COLUMN_BATTERY};
+
+ public static final String AUTHORITY = "nodomain.freeyourgadget.gadgetbridge.realtimesamples.provider";
+
+ public static final String ACTIVITY_START_URL = "content://" + AUTHORITY + "/activity_start";
+ public static final String ACTIVITY_STOP_URL = "content://" + AUTHORITY + "/activity_stop";
+ public static final String REALTIME_URL = "content://" + AUTHORITY + "/realtime";
+ public static final String DEVICES_URL = "content://" + AUTHORITY + "/devices";
+
+}
diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/database/SampleProviderTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/database/SampleProviderTest.java
index 77a8bb1bb5..4d682b231d 100644
--- a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/database/SampleProviderTest.java
+++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/database/SampleProviderTest.java
@@ -1,10 +1,26 @@
package nodomain.freeyourgadget.gadgetbridge.database;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.preference.PreferenceManager;
+import android.support.v4.content.LocalBroadcastManager;
+import android.util.Log;
+
+import org.junit.Ignore;
import org.junit.Test;
import java.util.List;
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.contentprovider.HRContentProvider;
+import nodomain.freeyourgadget.gadgetbridge.contentprovider.HRContentProviderContract;
+import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
@@ -12,6 +28,7 @@
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
+import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.test.TestBase;
import static org.junit.Assert.assertEquals;
@@ -19,14 +36,32 @@
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
+import org.robolectric.shadows.ShadowContentResolver;
+import org.robolectric.shadows.ShadowLog;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
public class SampleProviderTest extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(SampleProviderTest.class);
private GBDevice dummyGBDevice;
+ private ContentResolver mContentResolver;
@Override
public void setUp() throws Exception {
super.setUp();
+ ShadowLog.stream = System.out; // show logger’s output
+
dummyGBDevice = createDummyGDevice("00:00:00:00:10");
+ mContentResolver = app.getContentResolver();
+
+ HRContentProvider provider = new HRContentProvider();
+
+ // Stuff context into provider
+ provider.attachInfo(app.getApplicationContext(), null);
+
+ ShadowContentResolver.registerProviderInternal(HRContentProviderContract.AUTHORITY, provider);
}
@Test
@@ -122,7 +157,7 @@ public void testSamples() {
MiBandActivitySample s3 = createSample(sampleProvider, MiBandSampleProvider.TYPE_DEEP_SLEEP, 1200, 10, 62, 4030, user, device);
MiBandActivitySample s4 = createSample(sampleProvider, MiBandSampleProvider.TYPE_LIGHT_SLEEP, 2000, 10, 60, 4030, user, device);
- sampleProvider.addGBActivitySamples(new MiBandActivitySample[] { s3, s4 });
+ sampleProvider.addGBActivitySamples(new MiBandActivitySample[]{s3, s4});
// first checks for irrelevant timestamps => no samples
List samples = sampleProvider.getAllActivitySamples(0, 0);
@@ -170,4 +205,127 @@ public void testSamples() {
sleepSamples = sampleProvider.getSleepSamples(1500, 2500);
assertEquals(1, sleepSamples.size());
}
+
+ private void generateSampleStream(MiBandSampleProvider sampleProvider) {
+ final User user = DBHelper.getUser(daoSession);
+ final Device device = DBHelper.getDevice(dummyGBDevice, daoSession);
+
+ for (int i = 0; i < 10; i++) {
+ MiBandActivitySample sample = createSample(sampleProvider, MiBandSampleProvider.TYPE_ACTIVITY, 100 + i * 50, 10, 60 + i * 5, 1000 * i, user, device);
+ //LOG.debug("Sending sample " + sample.getHeartRate());
+ Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES)
+ .putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample);
+ LocalBroadcastManager.getInstance(app.getApplicationContext()).sendBroadcast(intent);
+ }
+ }
+
+
+ //@Ignore
+ @Test
+ public void testContentProvider() {
+
+ dummyGBDevice.setState(GBDevice.State.CONNECTED);
+ final MiBandSampleProvider sampleProvider = new MiBandSampleProvider(dummyGBDevice, daoSession);
+
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(app);
+ sharedPreferences.edit().putString(MiBandConst.PREF_MIBAND_ADDRESS, dummyGBDevice.getAddress()).commit();
+
+ // Refresh the device list
+ dummyGBDevice.sendDeviceUpdateIntent(app);
+
+ assertNotNull("The ContentResolver may not be null", mContentResolver);
+
+
+ Cursor cursor;
+ /*
+ * Test the device uri
+ */
+ cursor = mContentResolver.query(Uri.parse(HRContentProviderContract.DEVICES_URL), null, null, null, null);
+
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+
+ if (cursor.moveToFirst()) {
+ do {
+ String deviceName = cursor.getString(0);
+ String deviceAddress = cursor.getString(2);
+
+ assertEquals(dummyGBDevice.getName(), deviceName);
+ assertEquals(dummyGBDevice.getAddress(), deviceAddress);
+ } while (cursor.moveToNext());
+ }
+
+ /*
+ * Test the activity start uri
+ */
+ cursor = mContentResolver.query(Uri.parse(HRContentProviderContract.ACTIVITY_START_URL), null, null, null, null);
+ if (cursor.moveToFirst()) {
+ do {
+ String status = cursor.getString(0);
+ String message = cursor.getString(1);
+ assertEquals("OK", status);
+ assertEquals("Connected", message);
+
+ } while (cursor.moveToNext());
+ }
+
+ /*
+ * Test the activity stop uri
+ */
+ cursor = mContentResolver.query(Uri.parse(HRContentProviderContract.ACTIVITY_STOP_URL), null, null, null, null);
+ if (cursor.moveToFirst()) {
+ do {
+ String status = cursor.getString(0);
+ String message = cursor.getString(1);
+ assertEquals("OK", status);
+ assertEquals("No error", message);
+ } while (cursor.moveToNext());
+ }
+
+
+ /*
+ * Test realtime data and content observers
+ */
+ class A1 extends ContentObserver {
+ public int numObserved = 0;
+
+ A1() {
+ super(null);
+ }
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ super.onChange(selfChange, uri);
+ Cursor cursor = mContentResolver.query(Uri.parse(HRContentProviderContract.REALTIME_URL), null, null, null, null);
+ if (cursor.moveToFirst()) {
+ do {
+ String status = cursor.getString(0);
+ int heartRate = cursor.getInt(1);
+
+ LOG.info("HeartRate " + heartRate);
+ assertEquals("OK", status);
+ assertEquals(60 + 5*numObserved, heartRate);
+ } while (cursor.moveToNext());
+ }
+ numObserved++;
+ }
+ }
+ A1 a1 = new A1();
+
+ mContentResolver.registerContentObserver(Uri.parse(HRContentProviderContract.REALTIME_URL), false, a1);
+ generateSampleStream(sampleProvider);
+
+ assertEquals(a1.numObserved, 10);
+
+ }
+
+ @Test
+ public void testDeviceManager() {
+ DeviceManager manager = ((GBApplication) (this.getContext())).getDeviceManager();
+ Log.d("---------------", "-----------------------------------");
+
+ System.out.println("-----------------------------------------");
+ assertNotNull(((GBApplication) GBApplication.getContext()).getDeviceManager());
+ LOG.debug(manager.toString());
+
+ }
}