-
Notifications
You must be signed in to change notification settings - Fork 658
[WIP][RFC] Introduce a ContentProvider for HR Data #1138
base: master
Are you sure you want to change the base?
Changes from 10 commits
86038a7
1d1cd00
597076b
7c3ee13
8a03ff0
e9b1a39
c32763d
1fc49e9
ac46be1
c7331e5
9cd2f8e
6269b41
253c084
f80e947
d01e81b
26017dc
89b0a35
3f451f2
7de5284
019edf3
e51cf66
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
//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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"; | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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