Skip to content
This repository has been archived by the owner on Mar 25, 2024. It is now read-only.

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

Open
wants to merge 21 commits into
base: master
Choose a base branch
from

Conversation

boun
Copy link
Contributor

@boun boun commented Jul 4, 2018

Hi,

as I really want to get HR Data into runnerup here is a patch to do so. It is still not finished however I am proposing at an early stage to get some feedback. The Idea is to provide three query apis to:

  • Discover Device(s?) The Interface looks like you can pair multiple devices. Hence an endpoint to discover them. As I only have a MiBand2 I am not 100% sure how Gadgetbridge behaves here
  • Start / Stop Realtime DataCollection: It seems like I have to send an Intent to start that
  • Get Realtime Data: Send Samples and inform ContentObservers

I have written some tests. What works for sure is discovery of a device. Realtime data is currently delivered as String, that is also a TODO. Furthermore my activity_start / stop are a stub in the tests.

However before implementing Contract Classes, deciding on the data format, writing more tests etc. I would like to have some feedback, if the API meets your "taste", what Data you would send (only Heartrate, Heartrate and Steps, Full Sample Object). Furthermore some input on the Code, how to test everything is highly appreciated. If you think ContentProvider is not the way to go, that is also fine.

Have fun!

Benedikt Elser added 4 commits July 6, 2018 16:14
- Introduce a contract class for all constants
- Provide the device as parameter and try to connect to that
- Do not overwrite buffered_sample if info is bogus
- Bogus Measurements with -1 HeartRate should not trigger contentobservers
- Send HeartRate, Steps and Battery Level to callers
@boun
Copy link
Contributor Author

boun commented Jul 6, 2018

About the Travis error: The testsuite does not set up the DeviceManager and I have no idea how to do that. Strangely locally all tests pass.

Copy link
Contributor

@cpfeiffer cpfeiffer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot, very nice. And bonus points for the testcase 👍 👌

I haven't done much with content providers yet, but in general this looks good to me. Would this also start gadgetbridge in case it was not running before? And if BT is enabled, it should return an error, I suppose?

I think we should try not to provide RAW sample data, but normalized data, so that it would work with different devices. Atm, it's just HR and steps, so that's normalized already.

Great job!

@@ -413,6 +413,11 @@
android:authorities="com.getpebble.android.provider"
android:exported="true" />

<provider
android:name=".contentprovider.HRContentProvider"
android:authorities="com.gadgetbridge.heartrate.provider"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gadgetbridge.com is not our domain, gadgetbridge.org is. But why do you explicitly not use the nodomain.freeyourgadget.org name?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, I wanted to follow the name of the java packages, however on a second thought this should then have been nodomain.freeyourgadget.gadgetbridge. I'll change it to org.gadgetbridge. Because I a am tempted to keep the provider more general i'll go with

org.gadgetbridge.realtimesamples.provider

Feel free to oppose

@@ -0,0 +1,238 @@
/* Copyright (C) 2016-2018 Andreas Shimokawa, Carsten Pfeiffer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be your name, here :-)

LocalBroadcastManager.getInstance(this.getContext()).registerReceiver(mReceiver, filterLocal);

//TODO Do i need only for testing or also in production?
this.getContext().registerReceiver(mReceiver, filterLocal);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gadgetbridge's own events are sent via LocalBroadcastManager, so this should not be necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either way, for testing I need that hook. Maybe it is because I did not understand the testing framework completely. I'll have a look. I think there is a GBApplication.isLocal() call, so I can call it only if a testcase is running.

public static final String[] realtimeColumnNames = new String[]{"Status", "Heartrate", "Steps", "Battery"};

static final String AUTHORITY = "com.gadgetbridge.heartrate.provider";

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

com.gadgetbridge again..

// Stuff context into provider
provider.attachInfo(app.getApplicationContext(), null);

ShadowContentResolver.registerProviderInternal("com.gadgetbridge.heartrate.provider", provider);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

com.gadgetbr...

for (GBDevice device : deviceManager.getDevices()) {
if (deviceAddress.equals(device.getAddress())) {
Log.i(HRContentProvider.class.getName(), String.format("Found device device %s", device));
return device;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the slf4j logger instead of Log.i, Log, e, etc.

@boun
Copy link
Contributor Author

boun commented Jul 9, 2018

So I addressed your comments for the better part. What is left now to do is:

  • After some measurements I do not get any more heartrates from my Mi2BandSupport. This is unfortunate. It may have several reasons including: A bug in my code, a cursed firmware, a thread inside of GB which just shuts that reporting down. Any thoughts / pointers here? Could someone with another device test? Do I need to take a Wakelock somewhere?
  • Fix Travis: Tests fail because I try to query the device Manager which is not set up correctly (at least getDeviceManager() in GBApplication returns null) in the test environment. I am open to suggestions for workarounds.
  • Permission System to read HR Data: I think android asking for permission instead of just send HR Date to every interested party would be a nice thing to do

Benedikt Elser added 2 commits July 10, 2018 15:04
Introduce a timer that repeatedly asks the device to send measurements.
Found in LiveActivityFragment.
Strangely this works for Steps without punching it.
Needs testing
Copy link
Contributor

@cpfeiffer cpfeiffer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot, great job! 💪

I currently lack the time to investigate the problem with Travis. If all fails, we could @Ignore the testcase and fix it later.

Re permission for requesting HR data: yes, that totally makes sense.

Re stopping of hr measurements: the log should hopefully contain some information about the reason.

@@ -195,20 +198,27 @@ public Cursor query(@NonNull Uri uri, String[] projection, String selection, Str
@Nullable
private GBDevice getDevice(String deviceAddress) {
DeviceManager deviceManager;

if (mGBDevice != null && mGBDevice.getAddress() == deviceAddress) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be equals(), not ==

@@ -415,7 +415,7 @@

<provider
android:name=".contentprovider.HRContentProvider"
android:authorities="com.gadgetbridge.heartrate.provider"
android:authorities="org.gadgetbridge.realtimesamples.provider"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, but I still don't get the rationale: why org.gadgetbridge instead of nodomain.freeyourgadget.gadgetbridge? I'm not totally opposed, but I'd like to understand.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then I misunderstood you. I'll finally change it to nodomain.freeyourgadget.gadgetbridge

@boun
Copy link
Contributor Author

boun commented Jul 11, 2018

I found a Workaround for the missing samples problems. In LiveActivityFragment there is this:

GBApplication.deviceService().onEnableRealtimeHeartRateMeasurement(true);

which repeatedly calls that function. So I also added a Timer, that calls onEnableRealtimeHeartRateMeasurement over and over again. Though I think this is strange behaviour, as the realtimesteps keep coming without any periodic enabling... Anyway, everything works now.

I also added the permissions, hence I finished my todo list and will commit the changes to this branch.

@boun
Copy link
Contributor Author

boun commented Jul 13, 2018

O.k. I finally found the problem with the tests:

GBApplication is not set up correctly in onCreate(). It checks wether a static variable is set already and if so it returns. Rationale is to guard against multiple invocations from robolectric. However the DeviceManager is not static, therefore each invocation of the testsuite with more than one testcase will get null as a result for ((GBApplication)getContext()).getDeviceManager() in the second and following testcases.

@cpfeiffer actually it was you, who committed that change two years ago:

49b8b9e

I could now try to fix this, however this is core code which I do not know well enough.

A testcase is in my debug branch:
https://github.com/boun/Gadgetbridge/blob/debugging/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/SampleProviderTest2.java

It simply calls getDeviceManager() in two testcases. The first works, the second fails.

@cpfeiffer
Copy link
Contributor

cpfeiffer commented Jul 14, 2018

re timer: yes, we have to re-enable the realtime measurements again and again for Mi2. It is also kind of sensible, because delivering realtime data is much more battery intensive than sending the data in one go. So Mi2 only sends it for a short period of time if it doesn't get another enable-command.

@cpfeiffer
Copy link
Contributor

re DeviceManager: back then we got multiple invocations of GBApplication#onCreate() by robolectric, either due to a bug or by design. Things may have changed since then, so we might revise this. I totally agree that this code is a little convoluted, with the db being setup either by GBApplication (in normal mode) or by TestBase (in test mode). Initially, robolectric did not support custom Application subclasses at all and I don't know how it handles the Application instance lifecycle at the moment. For db tests, we want to make sure that every test gets a fresh db environment.

According to your findings, I suspect that we do get a new GBApplication instance (hence deviceManager field being null). The static fields survive, of course.

So to sum up,

  • we can check if we still get multiple onCreate() calls on the same instance
  • we can try to make all static instances in GBApplication non-static and then remove the guard
  • or we could make the deviceManager instance also static (urgh)
  • or come up with something else?

@ashimokawa
Copy link
Contributor

@cpfeiffer
I think we can have realtime measurements without sending commands over and over again. Not 100% sure though

@@ -758,6 +758,7 @@ public void onHeartRateTest() {
}
}

// Unser Einstig
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If these comments are intended to be submitted, they should be in English :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I managed to merge the [DO NOT MERGE] commit

@@ -1193,6 +1197,7 @@ public void doCurrentSample() {
try (DBHandler handler = GBApplication.acquireDB()) {
DaoSession session = handler.getDaoSession();

// Why is this so complicated????
Copy link
Contributor

@cpfeiffer cpfeiffer Jul 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have an idea how to make it less complicated?

}

@NonNull
private Cursor realtime(String[] projection, String[] args) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From looking at the name and the arguments, I would have no idea what it is or what arguments it expects. Maybe name it getRealtimeHeartRateCursor()?

}
return null;
}

@Nullable
private Cursor devices_list() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion for a better method name (and please try to use camel case): getDevicesCursor()? Or just getDevices()?

}


private void enable_realtime() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enableContinuousRealtimeHeartRateMeasurement()? Long, but descriptive.

@cpfeiffer
Copy link
Contributor

Thanks for your work, really looking forward to getting this in! 👍

@boun
Copy link
Contributor Author

boun commented Jul 16, 2018

About the tests: I did not see multiple invocations of onCreate during my (limited) tests. I saw one invocation per testcase. Hence from that point of view I should be o.k. to remove the guard.

Currently I am playing around with the integration of the contentprovider into runnerup, https://github.com/boun/runnerup/commits/contentprovider and some minor cleanup. When I am done I'd rebase, open up a new pull request and squash everything together?

Future Ideas for this provider would be:

  • Add a library via maven which contains the ContractClass for easier integration
  • Change the ContentProvider from this quite rudimentary buffered_sample in Memory thing to use the database as every realtime measurement automatically lands there too
  • Provide historic data

Benedikt Elser added 2 commits July 16, 2018 13:41
@boun
Copy link
Contributor Author

boun commented Jul 17, 2018

After removing the guard all 61 tests still pass. onCreate is called 61 times.

@boun
Copy link
Contributor Author

boun commented Jul 17, 2018

And to answer that question: Yes this starts gadgetbridge in case it was not running before.

Copy link
Contributor

@cpfeiffer cpfeiffer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll merge this in, great work! 👍 Do you want to squash anything before merging?

I'll add a tiny, a small and a slightly bigger TODO for a future update. For the tiny TODO, see the next comment (use HeartRateUtils).

The small one is that we need an entry for the changelog. This feature also warrants a blog post, but probably not before your patch for RunnerUp is merged, right?

The bigger TODO is that since Gadgetbridge is now being remote controlled, we should provide the user a means to stop this. E.g. in case an application is buggy, crashes, or forgets to stop the realtime measurement, the user should be able to stop it from within Gadgetbridge.

I think the easiest thing would be to have a notification to inform the user, that realtime measurement is being done at all, and it should have a "Stop" button to stop it. This would be independent of the content provider of course, it should also happen for the live activity tab. What do you think?

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better use HeartRateUtils.isValidHeartRateValue() here.

@boun
Copy link
Contributor Author

boun commented Jul 18, 2018

Sound lovely, I'll address the comment and squash it to a bare minimum. I'll ping you when I am done. In the meantime, might I motivate you to compile my runnerup fork and try it out with different / multiple devices? You don't need to go running, just some 5 mins of testing in your pocket would be fantastic.

Furthermore, for the record: as soon as this change is merged, you have an external api, which others will develop against. Hence I'd flag it as "beta" or "unstable" in the changelog entry for the first version.

@TaaviE
Copy link
Contributor

TaaviE commented Jul 21, 2018

I have one suggestion though, this content provider should be protected by an authorization dialogue before being released to the public and doing it right now while nothing depends on it sounds like a reasonable idea:

  1. Fragment for managing content provider accesses
  2. Database or shared preferences model for storing package names and granted access(es, if we add more providers later on)
  3. Content provider has to check calling package (For ex. ContentProvider.getCallingPackage)

@boun
Copy link
Contributor Author

boun commented Jul 21, 2018

Hi @TaaviE, thx for your comment. I think your wish is actually an OS function based on the android permissions system (see screenshot, the default is off, which I changed). Together with the suggestion by cpfeiffer (the big todo) it is guaranteed that the user first has to give his consent an then also sees an indicator that realtime monitoring is happening. And this will be the topic of my next comment :)

screenshot_1532211117

@boun
Copy link
Contributor Author

boun commented Jul 21, 2018

Hi @cpfeiffer, I like the idea of showing a notification to the user and allowing her to interrupt realtime sampling. However that is actually not as easy to do, because there is a thread inside the contentprovider (and LiveActivityFragment but that should not be the problem), that restarts that sampling each second.

Coincidently I do not like this implementation. And even more of a coincident is, that for my mi band 2 it is probably not correct either (https://github.com/Freeyourgadget/Gadgetbridge/issues/913). Hence should this not be moved up the stack closer to the "drivers"?

@TaaviE
Copy link
Contributor

TaaviE commented Jul 21, 2018

@boun Gadgetbridge's minimum API is Kitkat (19, 4.4) and runtime permissions are a Marshmallow (23+) thing meaning that this has to be implemented by Gadgetbridge.

@boun
Copy link
Contributor Author

boun commented Jul 22, 2018

@TaaviE It is not about runtime permissions. Here the screenshot from KitKat. I do not think it makes sense to rebuild a OS function, that is available in later releases. The Users is informed upon install that the app wants to access these services. And if the "big todo" is implemented she is informed in realtime and might even stop it from happening.

However this is my opinion, if you want to implement that feel free!

screenshot_1532261435

@TaaviE
Copy link
Contributor

TaaviE commented Jul 22, 2018

@boun Exactly my point, everyone who wants gets access when there's just that permission on KK. Say some proprietary app actually adds Gadgetbridge support I and I suspect many others would not want it to instantly have access to my heart rate data.

@boun
Copy link
Contributor Author

boun commented Jul 22, 2018

I consider your point highly hypothetical :) However feel free to implement it.

@danielegobbetti
Copy link
Contributor

Given the nature of the project, I agree that the feature outlined by @TaaviE is important and should be present when the new functionality is available in the published application.

Which means (in my opinion) that it could either be implemented in the PR, or after the merge (but before we release).

@cpfeiffer
Copy link
Contributor

I'm in a middle position. IMHO it shouldn't be a blocker for the first release. AFAIU, this is only a real problem for KK, where you cannot deny permissions. On Lollipop and above, a user could install a "rogue" app and simple deny the single permission.

A simple solution might be to have this feature on Lollipop+ only. Not sure if that would be a problem for the RunnerUp userbase. but I could imagine they wouldn't want to require a Gadgetbridge permission anyway, if they still targeted KK.

I personally had not the possibility to try out the integration yet, maybe next week.

@cpfeiffer
Copy link
Contributor

What's the current state of this? Can we move this forward?

@lesensei
Copy link

Hey there,
Since I'm very interested in being able to track my HR in an opensource running app, I tried this with my miband3. I probably did something wrong, because GB loses the connection to my mb3 after about 5 minutes and crashes when I try to export the logs from the debug screen. @boun if you're still interested in working on this, I can help test it but I'm no code wizard so I won't be able to do much by myself except run ADB and find out where it fails. I have runnerup from your contentprovider branch crashing as well when trying to get into the history pane, but I don't think this has anything to do with your changes.
Anyway, if I can help with something that's not way over my head, please let me know, I'd be happy to.
Regards,

@cpfeiffer
Copy link
Contributor

@lesensei Is this reproduceable? Can you provide logs (privately, if you want)?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants