diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bf3ae12fb..0d6b1836f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -413,6 +413,11 @@ android:authorities="com.getpebble.android.provider" android:exported="true" /> + + . */ +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.v4.content.LocalBroadcastManager; +import android.util.Log; + +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; + +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_ENABLE_REALTIME_HEARTRATE_MEASUREMENT; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_BOOLEAN_ENABLE; + +/** + * A content Provider, which publishes read only RAW @see ActivitySample to other applications + *

+ * TODO: + * For this to work there must be an additional api which switches the GadgetBridge to "start activity" + */ +public class HRContentProvider extends ContentProvider { + + 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; + + private static final UriMatcher URI_MATCHER; + + static { + URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); + URI_MATCHER.addURI(HRContentProvider.AUTHORITY, + "devices", DEVICES_LIST); + URI_MATCHER.addURI(HRContentProvider.AUTHORITY, + "realtime", REALTIME); + URI_MATCHER.addURI(HRContentProvider.AUTHORITY, + "activity_start", ACTIVITY_START); + URI_MATCHER.addURI(HRContentProvider.AUTHORITY, + "activity_stop", ACTIVITY_STOP); + } + + private ActivitySample buffered_sample = null; + + // this is only needed for the MatrixCursor constructor + public static final String[] deviceColumnNames = new String[]{"Name", "Model", "Address"}; + public static final String[] activityColumnNames = new String[]{"Status", "Message"}; + public static final String[] realtimeColumnNames = new String[]{"Status", "Message"}; + + static final String AUTHORITY = "com.gadgetbridge.heartrate.provider"; + 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); + + private GBDevice mGBDevice = null; + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + Log.e(HRContentProvider.class.getName(), "Received Event, aciton: " + action); + + switch (action) { + case GBDevice.ACTION_DEVICE_CHANGED: + mGBDevice = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE); + break; + case DeviceService.ACTION_REALTIME_SAMPLES: + buffered_sample = (ActivitySample) intent.getSerializableExtra(DeviceService.EXTRA_REALTIME_SAMPLE); + // This notifies the observer + getContext(). + getContentResolver(). + notifyChange(REALTIME_URI, null); + break; + default: + break; + } + + } + }; + + @Override + public boolean onCreate() { + Log.i(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); + + return true; + } + + @Override + public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + Log.e(HRContentProvider.class.getName(), "query uri " + uri.toString()); + MatrixCursor mc; + Intent intent; + + switch (URI_MATCHER.match(uri)) { + case DEVICES_LIST: + DeviceManager deviceManager = ((GBApplication) GBApplication.getContext()).getDeviceManager(); + List l = deviceManager.getDevices(); + Log.i(HRContentProvider.class.getName(), String.format("listing %d devices", l.size())); + + mc = new MatrixCursor(deviceColumnNames); + for (GBDevice dev : l) { + mc.addRow(new Object[]{dev.getName(), dev.getModel(), dev.getAddress()}); + } + return mc; + case ACTIVITY_START: + Log.i(HRContentProvider.class.getName(), String.format("Get ACTIVTY START")); + intent = + new Intent() + .setAction(ACTION_ENABLE_REALTIME_HEARTRATE_MEASUREMENT) + .putExtra(EXTRA_BOOLEAN_ENABLE, true); + + GBApplication.getContext().startService(intent); + mc = new MatrixCursor(activityColumnNames); + mc.addRow(new String[]{"OK", "No error"}); + return mc; + case ACTIVITY_STOP: + Log.i(HRContentProvider.class.getName(), String.format("Get ACTIVITY STOP")); + intent = + new Intent() + .setAction(ACTION_ENABLE_REALTIME_HEARTRATE_MEASUREMENT) + .putExtra(EXTRA_BOOLEAN_ENABLE, false); + + GBApplication.getContext().startService(intent); + mc = new MatrixCursor(activityColumnNames); + mc.addRow(new String[]{"OK", "No error"}); + return mc; + case REALTIME: + String sample_string = (buffered_sample == null) ? "" : buffered_sample.toString(); + Log.e(HRContentProvider.class.getName(), String.format("Get REALTIME buffered sample %s", sample_string)); + mc = new MatrixCursor(realtimeColumnNames); + + // TODO no strings... + mc.addRow(new Object[]{"OK", sample_string, buffered_sample}); + return mc; + } + return new MatrixCursor(deviceColumnNames); + } + + @Override + public String getType(@NonNull Uri uri) { + Log.e(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(); + } + +} \ No newline at end of file diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/database/SampleProviderTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/database/SampleProviderTest.java index 77a8bb1bb..736034227 100644 --- a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/database/SampleProviderTest.java +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/database/SampleProviderTest.java @@ -1,10 +1,24 @@ package nodomain.freeyourgadget.gadgetbridge.database; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +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.util.Log; + +import org.apache.tools.ant.types.resources.comparators.Content; import org.junit.Test; import java.util.List; +import nodomain.freeyourgadget.gadgetbridge.contentprovider.HRContentProvider; 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,21 +26,37 @@ 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; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.robolectric.Shadows.shadowOf; + +import org.robolectric.RuntimeEnvironment; +import org.robolectric.shadows.ShadowApplication; +import org.robolectric.shadows.ShadowContentResolver; + public class SampleProviderTest extends TestBase { private GBDevice dummyGBDevice; + private ContentResolver mContentResolver; @Override public void setUp() throws Exception { super.setUp(); 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("com.gadgetbridge.heartrate.provider", provider); } @Test @@ -122,7 +152,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 samples = sampleProvider.getAllActivitySamples(0, 0); @@ -170,4 +200,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.d(SampleProviderTest.class.getName(), "Sending sample " + sample.toString()); + Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES) + .putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample); + app.sendBroadcast(intent); + } + } + + + @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); + + /* + * (DeviceSupportFactory)mFactory.createDeviceSupport(gbDevice); --> + * deviceSupport = new ServiceDeviceSupport(new MiBand2Support(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING)); + * + */ + + /* + * Test the device uri + */ + Cursor cursor = mContentResolver.query(HRContentProvider.DEVICES_URI, 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(HRContentProvider.ACTIVITY_START_URI, 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 the activity start uri + */ + cursor = mContentResolver.query(HRContentProvider.ACTIVITY_STOP_URI, 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); + Log.e(SampleProviderTest.class.getName(), "Changed " + uri.toString()); + numObserved++; + } + }; + A1 a1 = new A1(); + + mContentResolver.registerContentObserver(HRContentProvider.REALTIME_URI, false, a1); + generateSampleStream(sampleProvider); + + /* + * Test the realtime data start uri + */ + cursor = mContentResolver.query(HRContentProvider.REALTIME_URI, null, null, null, null); + if (cursor.moveToFirst()) { + do { + String status = cursor.getString(0); + String message = cursor.getString(1); + assertEquals("OK", status); + assertEquals(false, message.isEmpty()); + Log.i("test", "------FOUND SOMETHING------"); + } while (cursor.moveToNext()); + } + + List l = shadowOf(RuntimeEnvironment.application).getBroadcastIntents(); + assertEquals(10, l.size()); + for (Intent i : l) + assertEquals(i.getAction(), DeviceService.ACTION_REALTIME_SAMPLES); + + List r = shadowOf(RuntimeEnvironment.application).getReceiversForIntent(l.get(0)); + assertEquals(1, r.size()); + + assertEquals(a1.numObserved, 10); + + } }