Skip to content
This repository has been archived by the owner on Dec 23, 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
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
}
Expand All @@ -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 changes: 8 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,6 @@ public void onCreate() {
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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
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.

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;
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.

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

}
Original file line number Diff line number Diff line change
@@ -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";

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..

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";

}
Loading