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 10 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")
}
}
}
5 changes: 5 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,11 @@
android:authorities="com.getpebble.android.provider"
android:exported="true" />

<provider
android:name=".contentprovider.HRContentProvider"
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

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
@@ -0,0 +1,248 @@
/* 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 android.util.Log;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;

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;

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;

// TODO: This is most of the time 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(HRContentProvider.class.getName(), "Received Event, aciton: " + action);

switch (action) {
case GBDevice.ACTION_DEVICE_CHANGED:
mGBDevice = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
LOG.debug(HRContentProvider.class.toString(), "Got Device " + mGBDevice);
// Rationale: If device was not connected
// it should show up here after beeing 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(HRContentProvider.class.toString(), "Device connected now, enabling realtime " + mGBDevice);

state = provider_state.ACTIVE;
GBApplication.deviceService().onEnableRealtimeSteps(true);
GBApplication.deviceService().onEnableRealtimeHeartRateMeasurement(true);
}
break;
case DeviceService.ACTION_REALTIME_SAMPLES:
ActivitySample tmp_sample = (ActivitySample) intent.getSerializableExtra(DeviceService.EXTRA_REALTIME_SAMPLE);
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(HRContentProviderContract.REALTIME_URI, null);
break;
default:
break;
}

}
};

@Override
public boolean onCreate() {
LOG.info(HRContentProvider.class.getName(), "Creating...");
IntentFilter filterLocal = new IntentFilter();

filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
filterLocal.addAction(DeviceService.ACTION_REALTIME_SAMPLES);

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.


//TODO: This crashes the app. Seems like device Manager is not here yet
//mGBDevice = ((GBApplication) this.getContext()).getDeviceManager().getSelectedDevice();

return true;
}

@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
//LOG.info(HRContentProvider.class.getName(), "query uri " + uri.toString());
MatrixCursor mc;

DeviceManager deviceManager;
switch (URI_MATCHER.match(uri)) {
case DEVICES_LIST:
deviceManager = ((GBApplication) (this.getContext())).getDeviceManager();
List<GBDevice> l = deviceManager.getDevices();
if (l == null) {
return null;
}
LOG.info(HRContentProvider.class.getName(), 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;
case ACTIVITY_START:

this.state = provider_state.CONNECTING;
LOG.info(HRContentProvider.class.getName(), "Get ACTIVTY START");
GBDevice targetDevice = getDevice((selectionArgs != null) ? selectionArgs[0] : "");
if (targetDevice != null && targetDevice.isConnected()) {
this.state = provider_state.ACTIVE;
GBApplication.deviceService().onEnableRealtimeSteps(true);
GBApplication.deviceService().onEnableRealtimeHeartRateMeasurement(true);
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;
case ACTIVITY_STOP:
this.state = provider_state.INACTIVE;

LOG.info(HRContentProvider.class.getName(), "Get ACTIVITY STOP");
GBApplication.deviceService().onEnableRealtimeSteps(false);
GBApplication.deviceService().onEnableRealtimeHeartRateMeasurement(false);
mc = new MatrixCursor(HRContentProviderContract.activityColumnNames);
mc.addRow(new String[]{"OK", "No error"});
return mc;
case REALTIME:
//String sample_string = (buffered_sample == null) ? "" : buffered_sample.toString();
//LOG.error(HRContentProvider.class.getName(), String.format("Get REALTIME buffered sample %s", sample_string));
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;
}
return null;
}

// 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() == 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 ==

LOG.info(HRContentProvider.class.getName(), 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(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.

}
}
LOG.info(HRContentProvider.class.getName(), String.format("Did not find device returning selected %s", deviceManager.getSelectedDevice()));
return deviceManager.getSelectedDevice();
}

@Override
public String getType(@NonNull Uri uri) {
LOG.error(HRContentProvider.class.getName(), "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,47 @@
/* 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.net.Uri;

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

static final String AUTHORITY = "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.

com.gadgetbridge again..

static final String ACTIVITY_START_URL = "content://" + AUTHORITY + "/activity_start";
static final String ACTIVITY_STOP_URL = "content://" + AUTHORITY + "/activity_stop";
static final String REALTIME_URL = "content://" + AUTHORITY + "/realtime";
static final String DEVICES_URL = "content://" + AUTHORITY + "/devices";

public static final Uri ACTIVITY_START_URI = Uri.parse(ACTIVITY_START_URL);
public static final Uri ACTIVITY_STOP_URI = Uri.parse(ACTIVITY_STOP_URL);
public static final Uri REALTIME_URI = Uri.parse(REALTIME_URL);
public static final Uri DEVICES_URI = Uri.parse(DEVICES_URL);
}
Loading