[WIP][RFC] Introduce a ContentProvider for HR Data #1138

Open
boun wants to merge 21 commits from boun/runnerup into master
  1. 8
      app/build.gradle
  2. 8
      app/src/main/AndroidManifest.xml
  3. 5
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java
  4. 288
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/contentprovider/HRContentProvider.java
  5. 41
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/contentprovider/HRContentProviderContract.java
  6. 160
      app/src/test/java/nodomain/freeyourgadget/gadgetbridge/database/SampleProviderTest.java

8
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")
}
}
}

8
app/src/main/AndroidManifest.xml

@ -34,6 +34,8 @@
android:name="android.hardware.telephony"
android:required="false" />
<permission android:name="nodomain.freeyourgadget.gadgetbridge.realtimesamples.provider.ACCESS_DATA"/>
<application
android:name=".GBApplication"
android:allowBackup="false"
@ -413,6 +415,12 @@
android:authorities="com.getpebble.android.provider"
android:exported="true" />
<provider
android:name=".contentprovider.HRContentProvider"
android:authorities="nodomain.freeyourgadget.gadgetbridge.realtimesamples.provider"
android:permission="nodomain.freeyourgadget.gadgetbridge.realtimesamples.provider.ACCESS_DATA"
android:exported="true" />
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.screenshot_provider"

5
app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java

@ -145,11 +145,6 @@ public class GBApplication extends Application {
app = this;
super.onCreate();
if (lockHandler != null) {
// guard against multiple invocations (robolectric)
return;
}
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs = new Prefs(sharedPrefs);
gbPrefs = new GBPrefs(prefs);

288
app/src/main/java/nodomain/freeyourgadget/gadgetbridge/contentprovider/HRContentProvider.java

@ -0,0 +1,288 @@
/* 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 <http://www.gnu.org/licenses/>. */
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
* <p>
*/
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<GBDevice> 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();
}
}

41
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 <http://www.gnu.org/licenses/>. */
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";
}

160
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.MiBandActivitySample;
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.assertNotNull;
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 class SampleProviderTest extends TestBase {
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<MiBandActivitySample> samples = sampleProvider.getAllActivitySamples(0, 0);
@ -170,4 +205,127 @@ public class SampleProviderTest extends TestBase {
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());
}
}

Loading…
Cancel
Save