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()); + + } }