diff --git a/.gitignore b/.gitignore index fefd7fedd471e9bc46cb9cf599d499b9a0b394ee..a0722a1d135612b37f3243c980473c0408b138e9 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ workspace.xml fabric.properties local.properties .idea +bugfender.properties diff --git a/app/app.iml b/app/app.iml index e21e32e18a1d39dd95c1e160e391b59f001dde94..841fa46dd6f3926ebd21f377de95524506efdd97 100644 --- a/app/app.iml +++ b/app/app.iml @@ -67,14 +67,6 @@ <sourceFolder url="file://$MODULE_DIR$/src/main/jni" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src/main/shaders" isTestSource="false" /> - <sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" /> - <sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" /> - <sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" /> - <sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" /> - <sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" /> - <sourceFolder url="file://$MODULE_DIR$/src/androidTest/jni" isTestSource="true" /> - <sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" /> - <sourceFolder url="file://$MODULE_DIR$/src/androidTest/shaders" isTestSource="true" /> <sourceFolder url="file://$MODULE_DIR$/src/test/res" type="java-test-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/test/assets" type="java-test-resource" /> @@ -83,8 +75,17 @@ <sourceFolder url="file://$MODULE_DIR$/src/test/jni" isTestSource="true" /> <sourceFolder url="file://$MODULE_DIR$/src/test/rs" isTestSource="true" /> <sourceFolder url="file://$MODULE_DIR$/src/test/shaders" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTest/jni" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTest/shaders" isTestSource="true" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/blame" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/builds" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundles" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/dependency-cache" /> @@ -94,7 +95,7 @@ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/recyclerview-v7/23.4.0/jars" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/support-v4/23.4.0/jars" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/support-vector-drawable/23.4.0/jars" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.bugfender.sdk/android/0.3.5/jars" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.bugfender.sdk/android/0.4/jars" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.crashlytics.sdk.android/answers/1.3.6/jars" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.crashlytics.sdk.android/beta/1.1.4/jars" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.crashlytics.sdk.android/crashlytics-core/2.3.8/jars" /> @@ -109,19 +110,31 @@ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/io.realm/realm-android-library/1.0.0/jars" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/uk.co.chrisjenx/calligraphy/2.2.0/jars" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental-classes" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental-runtime-classes" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental-safeguard" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental-verifier" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/instant-run-support" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/jniLibs" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/pre-dexed" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/reload-dex" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/restart-dex" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/shaders" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/transforms" /> <excludeFolder url="file://$MODULE_DIR$/build/outputs" /> <excludeFolder url="file://$MODULE_DIR$/build/tmp" /> </content> <orderEntry type="jdk" jdkName="Android API 23 Platform" jdkType="Android SDK" /> <orderEntry type="sourceFolder" forTests="false" /> + <orderEntry type="library" exported="" name="okio-1.8.0" level="project" /> <orderEntry type="library" exported="" name="support-annotations-23.4.0" level="project" /> <orderEntry type="library" exported="" name="google-material-typeface-2.2.0.1.original" level="project" /> + <orderEntry type="library" exported="" name="MPAndroidChart-v2.2.5" level="project" /> <orderEntry type="library" exported="" name="okhttp-2.4.0" level="project" /> <orderEntry type="library" exported="" name="relinker-1.2.1" level="project" /> <orderEntry type="library" exported="" name="retrofit-1.9.0" level="project" /> @@ -129,24 +142,25 @@ <orderEntry type="library" exported="" name="animated-vector-drawable-23.4.0" level="project" /> <orderEntry type="library" exported="" name="commons-lang3-3.4" level="project" /> <orderEntry type="library" exported="" name="support-v4-23.4.0" level="project" /> + <orderEntry type="library" exported="" name="android-0.4" level="project" /> <orderEntry type="library" exported="" name="recyclerview-v7-23.4.0" level="project" /> <orderEntry type="library" exported="" name="slf4j-api-1.7.2" level="project" /> <orderEntry type="library" exported="" name="support-vector-drawable-23.4.0" level="project" /> <orderEntry type="library" exported="" name="materialize-0.8.8" level="project" /> - <orderEntry type="library" exported="" name="okio-1.4.0" level="project" /> <orderEntry type="library" exported="" name="realm-android-library-1.0.0" level="project" /> <orderEntry type="library" exported="" name="appcompat-v7-23.4.0" level="project" /> <orderEntry type="library" exported="" name="fabric-1.3.10" level="project" /> <orderEntry type="library" exported="" name="design-23.4.0" level="project" /> - <orderEntry type="library" exported="" name="android-0.3.5" level="project" /> - <orderEntry type="library" exported="" name="gson-2.4" level="project" /> <orderEntry type="library" exported="" name="crashlytics-2.5.5" level="project" /> <orderEntry type="library" exported="" name="crashlytics-core-2.3.8" level="project" /> <orderEntry type="library" exported="" name="beta-1.1.4" level="project" /> <orderEntry type="library" exported="" name="fastadapter-1.5.2" level="project" /> - <orderEntry type="library" exported="" name="logback-android-1.1.1-3" level="project" /> + <orderEntry type="library" exported="" name="retrofit-2.1.0" level="project" /> + <orderEntry type="library" exported="" name="gson-2.7" level="project" /> + <orderEntry type="library" exported="" name="converter-gson-2.1.0" level="project" /> <orderEntry type="library" exported="" name="materialdrawer-5.2.9" level="project" /> <orderEntry type="library" exported="" name="calligraphy-2.2.0" level="project" /> + <orderEntry type="library" exported="" name="okhttp-3.3.0" level="project" /> <orderEntry type="library" exported="" name="answers-1.3.6" level="project" /> <orderEntry type="library" exported="" name="iconics-core-2.6.0" level="project" /> <orderEntry type="library" exported="" name="org.apache.http.legacy-android-23" level="project" /> diff --git a/app/build.gradle b/app/build.gradle index c767a6020438f130ef40e37285812e9b075fbf3a..0fa95d999428d38e08ad5f488c7cb844285ca4c7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -17,6 +17,7 @@ apply plugin: 'realm-android' repositories { maven { url 'https://maven.fabric.io/public' } + maven { url "https://jitpack.io" } } @@ -25,6 +26,12 @@ def gitVersion() { return process.toInteger() + 1 } +def getBugfenderApiKey(){ + Properties properties = new Properties() + properties.load(new FileInputStream("app/bugfender.properties")) + return "\"" + properties.getProperty("apiKey", "") +"\"" +} + android { compileSdkVersion 23 buildToolsVersion "23.0.3" @@ -41,6 +48,7 @@ android { targetSdkVersion 23 versionName project.properties['version'] versionCode gitVersion() + buildConfigField "String", "BUGFENDER_API_KEY", getBugfenderApiKey() } buildTypes { @@ -51,8 +59,9 @@ android { } lintOptions { - // TODO - 'InvalidPackage' is here because of logback. We can remove this if we remove logback. - disable 'InvalidPackage', 'TrulyRandom' + // Dag nabbit :( + // Because of http://stackoverflow.com/questions/35492259/lint-error-on-okio + warning 'InvalidPackage' } } @@ -113,9 +122,6 @@ release { } dependencies { - compile 'com.android.support:appcompat-v7:23.4.0' - compile 'org.apache.commons:commons-lang3:3.4' - compile files('libs/logback-android-1.1.1-3.jar') compile files('libs/slf4j-api-1.7.2.jar') compile('com.crashlytics.sdk.android:crashlytics:2.5.5@aar') { transitive = true; @@ -123,7 +129,14 @@ dependencies { compile('com.mikepenz:materialdrawer:5.2.9@aar') { transitive = true } + compile 'com.android.support:appcompat-v7:23.4.0' + compile 'org.apache.commons:commons-lang3:3.4' compile 'com.mikepenz:google-material-typeface:2.2.0.1.original@aar' compile 'uk.co.chrisjenx:calligraphy:2.2.0' - compile 'com.bugfender.sdk:android:0.3.5' + compile 'com.bugfender.sdk:android:0.4' + compile 'com.github.PhilJay:MPAndroidChart:v2.2.5' + compile 'com.android.support:support-v4:23.4.0' + compile 'com.google.code.gson:gson:2.7' + compile 'com.squareup.retrofit2:retrofit:2.1.0' + compile 'com.squareup.retrofit2:converter-gson:2.1.0' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c067681495750701d9a78af923eceb2d77fca1a0..b875e849aaf3675d64cbee090851630aa761a929 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -32,13 +32,10 @@ android:launchMode="singleTask" android:screenOrientation="portrait"> <intent-filter android:icon="@drawable/ic_launcher"> - <action android:name="android.intent.action.MAIN" /> - <category android:name="android.intent.category.DEFAULT" /> - <category android:name="android.intent.category.LAUNCHER" /> - </intent-filter> - <intent-filter android:icon="@drawable/ic_launcher"> + <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" /> + <action android:name="android.hardware.usb.action.USB_DEVICE_DETACHED" /> </intent-filter> <meta-data @@ -49,17 +46,30 @@ android:name=".settings.SettingsActivity" android:icon="@drawable/ic_launcher" android:label="Settings" - android:theme="@style/SettingsTheme"/> + android:theme="@style/SettingsTheme" /> + <activity + android:name=".SplashActivity" + android:theme="@style/SplashTheme"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> <activity android:name=".medtronic.GetHmacAndKeyActivity" android:label="@string/title_activity_login" android:theme="@style/SettingsTheme" /> + <service + android:name=".upload.nightscout.NightscoutUploadIntentService" + android:icon="@drawable/ic_launcher" /> + <service android:name=".medtronic.service.MedtronicCnlIntentService" - android:icon="@drawable/ic_launcher"></service> + android:icon="@drawable/ic_launcher" /> + <activity android:name=".medtronic.StatusActivity"></activity> </application> </manifest> \ No newline at end of file diff --git a/app/src/main/assets/logback.xml b/app/src/main/assets/logback.xml deleted file mode 100644 index c6459d39ae7475308c13b18602fdbd9fbd217d83..0000000000000000000000000000000000000000 --- a/app/src/main/assets/logback.xml +++ /dev/null @@ -1,23 +0,0 @@ -<configuration> - <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> - <file>/sdcard/640gAndroidUploader.log</file> - <append>true</append> - <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy"> - <fileNamePattern>/sdcard/640gAndroidUploader.%i.log.zip</fileNamePattern> - <minIndex>1</minIndex> - <maxIndex>3</maxIndex> - </rollingPolicy> - - <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"> - <maxFileSize>5MB</maxFileSize> - </triggeringPolicy> - - <encoder> - <pattern>%-5p[%d{yyyy-MM-dd HH:mm:ss}]:%msg%n</pattern> - </encoder> - </appender> - - <root level="ERROR"> - <appender-ref ref="FILE" /> - </root> -</configuration> \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/android/SplashActivity.java b/app/src/main/java/info/nightscout/android/SplashActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..e84fa6f22e10025788b7c2573205af93fc67855a --- /dev/null +++ b/app/src/main/java/info/nightscout/android/SplashActivity.java @@ -0,0 +1,21 @@ +package info.nightscout.android; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; + +import info.nightscout.android.medtronic.MainActivity; + +/** + * Created by lgoedhart on 18/06/2016. + */ +public class SplashActivity extends AppCompatActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Intent intent = new Intent(this, MainActivity.class); + startActivity(intent); + finish(); + } +} diff --git a/app/src/main/java/info/nightscout/android/USB/UsbHidDriver.java b/app/src/main/java/info/nightscout/android/USB/UsbHidDriver.java index a8d625678b39a3d0d990534027dc84e48274d430..ea63fa44d6f6f2019bfae1345156a09cd1a114ab 100644 --- a/app/src/main/java/info/nightscout/android/USB/UsbHidDriver.java +++ b/app/src/main/java/info/nightscout/android/USB/UsbHidDriver.java @@ -34,20 +34,27 @@ public class UsbHidDriver extends CommonUsbDriver { super(device, connection); } - public static UsbHidDriver acquire(UsbManager usbManager, int vendorId, int productId) { - + public static UsbDevice getUsbDevice(UsbManager usbManager, int vendorId, int productId) { // Iterate all the available devices and find ours. for (UsbDevice device : usbManager.getDeviceList().values()) { if (device.getProductId() == productId && device.getVendorId() == vendorId) { - final UsbDeviceConnection mConnection = usbManager.openDevice(device); - - return new UsbHidDriver(device, mConnection); + return device; } } return null; } + public static UsbHidDriver acquire(UsbManager usbManager, UsbDevice device) { + if (device != null) { + final UsbDeviceConnection mConnection = usbManager.openDevice(device); + + return new UsbHidDriver(device, mConnection); + } + + return null; + } + @Override public void open() throws IOException { Log.d(TAG, "Claiming HID interface."); diff --git a/app/src/main/java/info/nightscout/android/UploaderApplication.java b/app/src/main/java/info/nightscout/android/UploaderApplication.java index 384ab6f2f39319ba3fcbeebf4b3232d9e266d141..13fe3d456d7a3afbd0368ca2e10a47d1dbd5ec53 100644 --- a/app/src/main/java/info/nightscout/android/UploaderApplication.java +++ b/app/src/main/java/info/nightscout/android/UploaderApplication.java @@ -1,9 +1,14 @@ package info.nightscout.android; import android.app.Application; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; import com.bugfender.sdk.Bugfender; +import com.crashlytics.android.Crashlytics; +import com.crashlytics.android.answers.Answers; +import io.fabric.sdk.android.Fabric; import io.realm.Realm; import io.realm.RealmConfiguration; import uk.co.chrisjenx.calligraphy.CalligraphyConfig; @@ -21,8 +26,20 @@ public class UploaderApplication extends Application { .build() ); - //Bugfender.init(this, "TSwASSlwqOGXFb86RadI7EEJZSr2jGeT", BuildConfig.DEBUG); - //Bugfender.enableLogcatLogging(); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext()); + + if (prefs.getBoolean(getString(R.string.preferences_enable_crashlytics), true)) { + Fabric.with(this, new Crashlytics()); + } + if (prefs.getBoolean(getString(R.string.preferences_enable_answers), true)) { + Fabric.with(this, new Answers()); + } + + if (prefs.getBoolean(getString(R.string.preferences_enable_remote_logcat), false)) { + Bugfender.init(this, BuildConfig.BUGFENDER_API_KEY, BuildConfig.DEBUG); + Bugfender.enableLogcatLogging(); + Bugfender.setDeviceString("NightscoutURL", prefs.getString(getString(R.string.preference_nightscout_url), "Not set")); + } RealmConfiguration realmConfiguration = new RealmConfiguration.Builder(this).build(); Realm.setDefaultConfiguration(realmConfiguration); diff --git a/app/src/main/java/info/nightscout/android/medtronic/GetHmacAndKeyActivity.java b/app/src/main/java/info/nightscout/android/medtronic/GetHmacAndKeyActivity.java index e96266b00e2f6b9b9225ff02dc8f3262462070ca..64c30b3aea050802499e70eac5b49b00e373fbea 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/GetHmacAndKeyActivity.java +++ b/app/src/main/java/info/nightscout/android/medtronic/GetHmacAndKeyActivity.java @@ -5,12 +5,16 @@ import android.animation.AnimatorListenerAdapter; import android.annotation.TargetApi; import android.app.LoaderManager.LoaderCallbacks; import android.content.Context; +import android.content.DialogInterface; import android.content.Loader; import android.database.Cursor; import android.graphics.Color; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; +import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.text.Html; import android.text.TextUtils; @@ -87,7 +91,7 @@ public class GetHmacAndKeyActivity extends AppCompatActivity implements LoaderCa mPasswordView.setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView textView, int id, KeyEvent keyEvent) { - if (id == R.id.login || id == EditorInfo.IME_NULL) { + if (id == EditorInfo.IME_ACTION_DONE) { attemptLogin(); return true; } @@ -137,7 +141,7 @@ public class GetHmacAndKeyActivity extends AppCompatActivity implements LoaderCa * errors are presented and no actual login attempt is made. */ private void attemptLogin() { - if (mHmacAndKeyTask != null) { + if (mHmacAndKeyTask != null || !checkOnline("Please connect to the Internet", "You must be online to register your USB stick.")) { return; } @@ -232,6 +236,29 @@ public class GetHmacAndKeyActivity extends AppCompatActivity implements LoaderCa mRegisteredStickView.setText(Html.fromHtml(deviceTableHtml)); } + private boolean checkOnline(String title, String message) { + ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo netInfo = cm.getActiveNetworkInfo(); + + boolean isOnline = (netInfo != null && netInfo.isConnectedOrConnecting()); + + if (!isOnline) { + new AlertDialog.Builder(this, R.style.AppTheme) + .setTitle(title) + .setMessage(message) + .setCancelable(false) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }) + .setIcon(android.R.drawable.ic_dialog_alert) + .show(); + } + + return isOnline; + } + @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { return null; diff --git a/app/src/main/java/info/nightscout/android/medtronic/MainActivity.java b/app/src/main/java/info/nightscout/android/medtronic/MainActivity.java index 887ab956f4f73d4615bb8e599afaa6bc5037ac20..9c632ee91d22a1c65128f9215a2e42ae3eb1c11c 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/MainActivity.java +++ b/app/src/main/java/info/nightscout/android/medtronic/MainActivity.java @@ -1,7 +1,7 @@ package info.nightscout.android.medtronic; -import android.app.ActivityManager; import android.app.AlarmManager; +import android.app.NotificationManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; @@ -10,29 +10,32 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbManager; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; import android.os.BatteryManager; import android.os.Bundle; +import android.os.Handler; import android.preference.PreferenceManager; +import android.support.v4.app.TaskStackBuilder; import android.support.v4.content.LocalBroadcastManager; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; +import android.support.v7.app.NotificationCompat; import android.support.v7.widget.Toolbar; import android.text.Html; import android.text.format.DateUtils; import android.util.Log; import android.view.Menu; +import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import android.view.View.OnClickListener; -import android.widget.Button; import android.widget.TextView; import android.widget.TextView.BufferType; -import com.crashlytics.android.Crashlytics; -import com.crashlytics.android.answers.Answers; +import com.github.mikephil.charting.data.realm.implementation.RealmLineData; +import com.github.mikephil.charting.data.realm.implementation.RealmLineDataSet; +import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; +import com.github.mikephil.charting.utils.ColorTemplate; import com.mikepenz.google_material_typeface_library.GoogleMaterial; import com.mikepenz.materialdrawer.AccountHeaderBuilder; import com.mikepenz.materialdrawer.Drawer; @@ -40,78 +43,67 @@ import com.mikepenz.materialdrawer.DrawerBuilder; import com.mikepenz.materialdrawer.model.PrimaryDrawerItem; import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Date; import java.util.Locale; import info.nightscout.android.R; +import info.nightscout.android.USB.UsbHidDriver; import info.nightscout.android.eula.Eula; import info.nightscout.android.eula.Eula.OnEulaAgreedTo; import info.nightscout.android.medtronic.service.MedtronicCnlIntentService; +import info.nightscout.android.model.CgmStatusEvent; +import info.nightscout.android.model.medtronicNg.ContourNextLinkInfo; import info.nightscout.android.settings.SettingsActivity; -import info.nightscout.android.upload.MedtronicNG.CGMRecord; import info.nightscout.android.upload.MedtronicNG.PumpStatusRecord; -import io.fabric.sdk.android.Fabric; +import info.nightscout.android.upload.nightscout.NightscoutUploadIntentService; +import io.realm.Realm; +import io.realm.RealmResults; +import io.realm.Sort; import uk.co.chrisjenx.calligraphy.CalligraphyContextWrapper; /* Main activity for the MainActivity program */ public class MainActivity extends AppCompatActivity implements OnSharedPreferenceChangeListener, OnEulaAgreedTo { private static final String TAG = MainActivity.class.getSimpleName(); + public static int batLevel = 0; public static PumpStatusRecord pumpStatusRecord = new PumpStatusRecord(); - final Object mHandlerActiveLock = new Object(); - boolean keepServiceAlive = true; - Boolean mHandlerActive = false; - ActivityManager manager = null; + boolean mEnableCgmService = true; SharedPreferences prefs = null; - private Logger log = (Logger) LoggerFactory.getLogger(MainActivity.class.getName()); - private Toolbar toolbar; - private TextView mTextViewBg; - private TextView mTextViewBgTime; - private TextView mTextViewLog; - private TextView mTextViewUnits; - private TextView mTextViewTrend; - private TextView mTextViewIOB; - private Button buttonStopService; + private TextView mTextViewLog; // This will eventually move to a status page. private Intent mCnlIntentService; + private Intent mNightscoutUploadService; + private Handler mUiRefreshHandler = new Handler(); + private Runnable mUiRefreshRunnable = new RefreshDisplayRunnable(); + private Realm mRealm; - //Look for and launch the service, mTextViewLog status to user @Override public void onCreate(Bundle savedInstanceState) { Log.i(TAG, "onCreate called"); super.onCreate(savedInstanceState); + mRealm = Realm.getDefaultInstance(); mCnlIntentService = new Intent(this, MedtronicCnlIntentService.class); + mNightscoutUploadService = new Intent(this, NightscoutUploadIntentService.class); setContentView(R.layout.activity_main); PreferenceManager.getDefaultSharedPreferences(getBaseContext()).registerOnSharedPreferenceChangeListener(this); prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext()); - if (prefs.getBoolean(getString(R.string.preferences_enable_crashlytics), true)) { - Fabric.with(this, new Crashlytics()); - } - if (prefs.getBoolean(getString(R.string.preferences_enable_answers), true)) { - Fabric.with(this, new Answers()); - } - - if (!prefs.getBoolean("IUNDERSTAND", false)) { + if (!prefs.getBoolean(getString(R.string.preference_eula_accepted), false)) { stopCgmService(); - } else { - mHandlerActive = true; } - // Registers the DownloadStateReceiver and its intent filters LocalBroadcastManager.getInstance(this).registerReceiver( new StatusMessageReceiver(), new IntentFilter(MedtronicCnlIntentService.Constants.ACTION_STATUS_MESSAGE)); LocalBroadcastManager.getInstance(this).registerReceiver( - new CgmRecordReceiver(), - new IntentFilter(MedtronicCnlIntentService.Constants.ACTION_CGM_DATA)); + new RefreshDataReceiver(), + new IntentFilter(MedtronicCnlIntentService.Constants.ACTION_REFRESH_DATA)); - keepServiceAlive = Eula.show(this, prefs); + mEnableCgmService = Eula.show(this, prefs); IntentFilter batteryIntentFilter = new IntentFilter(); batteryIntentFilter.addAction(Intent.ACTION_BATTERY_LOW); @@ -119,13 +111,20 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc batteryIntentFilter.addAction(Intent.ACTION_BATTERY_OKAY); registerReceiver(new BatteryReceiver(), batteryIntentFilter); + UsbReceiver usbReceiver = new UsbReceiver(); IntentFilter usbIntentFilter = new IntentFilter(); usbIntentFilter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); usbIntentFilter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); - registerReceiver(new UsbReceiver(), usbIntentFilter); + usbIntentFilter.addAction(MedtronicCnlIntentService.Constants.ACTION_USB_PERMISSION); + registerReceiver(usbReceiver, usbIntentFilter); + LocalBroadcastManager.getInstance(this).registerReceiver( + usbReceiver, + new IntentFilter(MedtronicCnlIntentService.Constants.ACTION_NO_USB_PERMISSION)); + LocalBroadcastManager.getInstance(this).registerReceiver( + usbReceiver, + new IntentFilter(MedtronicCnlIntentService.Constants.ACTION_USB_REGISTER)); - manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); - toolbar = (Toolbar) findViewById(R.id.toolbar); + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); if (toolbar != null) { setSupportActionBar(toolbar); @@ -134,12 +133,28 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc getSupportActionBar().setTitle("Nightscout"); } - - final PrimaryDrawerItem itemHome = new PrimaryDrawerItem().withName("Home").withIcon(GoogleMaterial.Icon.gmd_home).withSelectable(false); - final PrimaryDrawerItem itemSettings = new PrimaryDrawerItem().withName("Settings").withIcon(GoogleMaterial.Icon.gmd_settings).withSelectable(false); - final PrimaryDrawerItem itemRegisterUsb = new PrimaryDrawerItem().withName("Register Contour Next Link").withIcon(GoogleMaterial.Icon.gmd_usb).withSelectable(false); - - Drawer drawer = new DrawerBuilder() + final PrimaryDrawerItem itemSettings = new PrimaryDrawerItem() + .withName("Settings") + .withIcon(GoogleMaterial.Icon.gmd_settings) + .withSelectable(false); + final PrimaryDrawerItem itemRegisterUsb = new PrimaryDrawerItem() + .withName("Register Contour Next Link") + .withIcon(GoogleMaterial.Icon.gmd_usb) + .withSelectable(false); + final PrimaryDrawerItem itemStopCollecting = new PrimaryDrawerItem() + .withName("Stop collecting data") + .withIcon(GoogleMaterial.Icon.gmd_stop) + .withSelectable(false); + final PrimaryDrawerItem itemGetNow = new PrimaryDrawerItem() + .withName("Read data now") + .withIcon(GoogleMaterial.Icon.gmd_play_arrow) + .withSelectable(false); + final PrimaryDrawerItem itemClearLog = new PrimaryDrawerItem() + .withName("Clear Log") + .withIcon(GoogleMaterial.Icon.gmd_clear_all) + .withSelectable(false); + + new DrawerBuilder() .withActivity(this) .withAccountHeader(new AccountHeaderBuilder() .withActivity(this) @@ -151,9 +166,11 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc .withActionBarDrawerToggle(true) .withSelectedItem(-1) .addDrawerItems( - itemHome, itemSettings, - itemRegisterUsb + itemRegisterUsb, + itemStopCollecting, + itemGetNow, + itemClearLog ) .withOnDrawerItemClickListener(new Drawer.OnDrawerItemClickListener() { @Override @@ -162,74 +179,29 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc openSettings(); } else if (drawerItem.equals(itemRegisterUsb)) { openUsbRegistration(); + } else if (drawerItem.equals(itemStopCollecting)) { + mEnableCgmService = false; + stopCgmService(); + finish(); + } else if (drawerItem.equals(itemGetNow)) { + startCgmService(); + } else if (drawerItem.equals(itemClearLog)) { + mTextViewLog.setText("", BufferType.EDITABLE); } + return false; } }) .build(); - // UI elements - TODO do these need to be members? - mTextViewBg = (TextView) findViewById(R.id.textview_bg); - mTextViewBgTime = (TextView) findViewById(R.id.textview_bg_time); mTextViewLog = (TextView) findViewById(R.id.textview_log); - mTextViewUnits = (TextView) findViewById(R.id.textview_units); - if (prefs.getBoolean("mmolxl", false)) { - mTextViewUnits.setText(R.string.text_unit_mmolxl); - } else { - mTextViewUnits.setText(R.string.text_unit_mgxdl); - } - mTextViewTrend = (TextView) findViewById(R.id.textview_trend); - mTextViewIOB = (TextView) findViewById(R.id.textview_iob); - - buttonStopService = (Button) findViewById(R.id.button_stop_service); - - Button buttonClearLog = (Button) findViewById(R.id.button_clear_log); - Button buttonStartService = (Button) findViewById(R.id.button_start_service); - - buttonClearLog.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - mTextViewLog.setText("", BufferType.EDITABLE); - } - }); - - buttonStartService.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - // Get an immediate reading. - startCgmService(); - } - }); - - buttonStopService.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - synchronized (mHandlerActiveLock) { - mHandlerActive = false; - keepServiceAlive = false; - stopCgmService(); - finish(); - } - } - }); } @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); startCgmService(); - } - - @Override - protected void onPause() { - log.info("ON PAUSE!"); - super.onPause(); - } - - @Override - protected void onResume() { - log.info("ON RESUME!"); - super.onResume(); + startDisplayRefreshLoop(); } @Override @@ -239,22 +211,28 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc @Override public boolean onCreateOptionsMenu(Menu menu) { - //MenuInflater inflater = getMenuInflater(); - //inflater.inflate(R.menu.menu, menu); + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.menu, menu); return true; } - private boolean checkOnline(String title, String message) { - ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo netInfo = cm.getActiveNetworkInfo(); - - boolean isOnline = (netInfo != null && netInfo.isConnectedOrConnecting()); + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_menu_status: + Intent intent = new Intent(this, StatusActivity.class); + startActivity(intent); + break; + } + return true; + } - if (!isOnline) { + private boolean hasDetectedCnl() { + if (mRealm.where(ContourNextLinkInfo.class).count() == 0) { new AlertDialog.Builder(this, R.style.AppTheme) - .setTitle(title) - .setMessage(message) + .setTitle("Contour Next Link not detected") + .setMessage("To register a Contour Next Link you must first plug it in.") .setCancelable(false) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { @@ -263,38 +241,80 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc }) .setIcon(android.R.drawable.ic_dialog_alert) .show(); + return false; } + return true; + } + + private boolean hasUsbPermission() { + UsbManager usbManager = (UsbManager) this.getSystemService(Context.USB_SERVICE); + UsbDevice cnlDevice = UsbHidDriver.getUsbDevice(usbManager, MedtronicCnlIntentService.USB_VID, MedtronicCnlIntentService.USB_PID); + + return !(usbManager != null && cnlDevice != null && !usbManager.hasPermission(cnlDevice)); - return isOnline; } - @Override - public boolean onOptionsItemSelected(MenuItem item) { + private void waitForUsbPermission() { + AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + Intent permissionIntent = new Intent(MedtronicCnlIntentService.Constants.ACTION_USB_PERMISSION); + permissionIntent.putExtra(UsbManager.EXTRA_PERMISSION_GRANTED, hasUsbPermission()); + PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, permissionIntent, PendingIntent.FLAG_UPDATE_CURRENT); + alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 1000L, pendingIntent); + } - switch (item.getItemId()) { - case R.id.menu_settings: - Intent settingsIntent = new Intent(this, SettingsActivity.class); - startActivity(settingsIntent); - break; - case R.id.registerCNL: - if (checkOnline("Please connect to the Internet", "You must be online to register your USB stick.")) { - Intent loginIntent = new Intent(this, GetHmacAndKeyActivity.class); - startActivity(loginIntent); - } - break; - default: - break; + private void requestUsbPermission() { + if (!hasUsbPermission()) { + UsbManager usbManager = (UsbManager) this.getSystemService(Context.USB_SERVICE); + UsbDevice cnlDevice = UsbHidDriver.getUsbDevice(usbManager, MedtronicCnlIntentService.USB_VID, MedtronicCnlIntentService.USB_PID); + PendingIntent permissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(MedtronicCnlIntentService.Constants.ACTION_USB_PERMISSION), 0); + usbManager.requestPermission(cnlDevice, permissionIntent); } - return super.onOptionsItemSelected(item); + } + + private void refreshDisplay() { + cancelDisplayRefreshLoop(); + startDisplayRefreshLoop(); + } + + private void startDisplayRefreshLoop() { + mUiRefreshHandler.post(mUiRefreshRunnable); + } + + private void cancelDisplayRefreshLoop() { + mUiRefreshHandler.removeCallbacks(mUiRefreshRunnable); } private void startCgmService() { + startCgmService(0L); + } + + private void startCgmService(long runAtTime) { Log.i(TAG, "startCgmService called"); - startService(mCnlIntentService); + + if (!mEnableCgmService) { + return; + } + + if (runAtTime == 0L) { + startService(mCnlIntentService); + } else { + AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + PendingIntent pending = PendingIntent.getService(this, 0, mCnlIntentService, 0); + + alarmManager.set(AlarmManager.RTC_WAKEUP, runAtTime, pending); + } } private void startCgmServicePolling(long initialPoll) { Log.i(TAG, "startCgmServicePolling called"); + + if (!mEnableCgmService) { + return; + } + + // Cancel any existing polling. + stopCgmService(); + AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); PendingIntent pending = PendingIntent.getService(this, 0, mCnlIntentService, 0); @@ -302,6 +322,10 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc initialPoll, MedtronicCnlIntentService.POLL_PERIOD_MS, pending); } + private void uploadCgmData() { + startService(mNightscoutUploadService); + } + private void stopCgmService() { Log.i(TAG, "stopCgmService called"); @@ -311,21 +335,49 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc alarmManager.cancel(pending); } + private void showDisconnectionNotification(String title, String message) { + int notifyId = 1; + + NotificationCompat.Builder mBuilder = + (NotificationCompat.Builder) new NotificationCompat.Builder(this) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setSmallIcon(R.drawable.ic_launcher) // FIXME - this icon doesn't follow the standards (ie, it has black in it) + .setContentTitle(title) + .setContentText(message) + .setTicker(message) + .setVibrate(new long[]{1000, 1000, 1000, 1000, 1000}); + // Creates an explicit intent for an Activity in your app + Intent resultIntent = new Intent(this, MainActivity.class); + + TaskStackBuilder stackBuilder = TaskStackBuilder.create(this); + // Adds the back stack for the Intent (but not the Intent itself) + stackBuilder.addParentStack(MainActivity.class); + // Adds the Intent that starts the Activity to the top of the stack + stackBuilder.addNextIntent(resultIntent); + PendingIntent resultPendingIntent = + stackBuilder.getPendingIntent( + 0, + PendingIntent.FLAG_UPDATE_CURRENT + ); + mBuilder.setContentIntent(resultPendingIntent); + NotificationManager mNotificationManager = + (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + // notifyId allows you to update the notification later on. + mNotificationManager.notify(notifyId, mBuilder.build()); + } + @Override protected void onDestroy() { Log.i(TAG, "onDestroy called"); - log.info("onDestroy called"); PreferenceManager.getDefaultSharedPreferences(getBaseContext()).unregisterOnSharedPreferenceChangeListener(this); - synchronized (mHandlerActiveLock) { - if (!keepServiceAlive) { - stopCgmService(); - } - mHandlerActive = false; - SharedPreferences.Editor editor = getBaseContext().getSharedPreferences(MedtronicConstants.PREFS_NAME, 0).edit(); - editor.putLong("lastDestroy", System.currentTimeMillis()); - editor.apply(); - super.onDestroy(); + cancelDisplayRefreshLoop(); + + if (!mEnableCgmService) { + stopCgmService(); } + + mRealm.close(); + super.onDestroy(); } @Override @@ -333,25 +385,25 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc String key) { if (key.equals(getString(R.string.preference_eula_accepted))) { if (!sharedPreferences.getBoolean(getString(R.string.preference_eula_accepted), false)) { - synchronized (mHandlerActiveLock) { - mHandlerActive = false; - } + mEnableCgmService = false; stopCgmService(); } else { + mEnableCgmService = true; startCgmService(); - mHandlerActive = true; } + } else if (key.equals("mmolxl")) { + refreshDisplay(); } } @Override public void onEulaAgreedTo() { - keepServiceAlive = true; + mEnableCgmService = true; } @Override public void onEulaRefusedTo() { - keepServiceAlive = false; + mEnableCgmService = false; } public void openSettings() { @@ -360,13 +412,13 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc } public void openUsbRegistration() { - if (checkOnline("Please connect to the Internet", "You must be online to register your USB stick.")) { + if (hasDetectedCnl()) { Intent loginIntent = new Intent(this, GetHmacAndKeyActivity.class); startActivity(loginIntent); } } - private String renderTrendHtml(CGMRecord.TREND trend) { + private String renderTrendHtml(CgmStatusEvent.TREND trend) { switch (trend) { case DOUBLE_UP: return "⇈"; @@ -392,18 +444,41 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc public void onReceive(Context context, Intent intent) { String message = intent.getStringExtra(MedtronicCnlIntentService.Constants.EXTENDED_DATA); + Log.i(TAG, "Message Receiver: " + message); mTextViewLog.setText(mTextViewLog.getText() + "\n" + message, BufferType.EDITABLE); } } - private class CgmRecordReceiver extends BroadcastReceiver { + private class RefreshDisplayRunnable implements Runnable { @Override - public void onReceive(Context context, Intent intent) { - CGMRecord cgmRecord = (CGMRecord) intent.getSerializableExtra(MedtronicCnlIntentService.Constants.EXTENDED_DATA); + public void run() { + // UI elements - TODO do these need to be members? + TextView textViewBg = (TextView) findViewById(R.id.textview_bg); + TextView textViewBgTime = (TextView) findViewById(R.id.textview_bg_time); + TextView textViewUnits = (TextView) findViewById(R.id.textview_units); + if (prefs.getBoolean("mmolxl", false)) { + textViewUnits.setText(R.string.text_unit_mmolxl); + } else { + textViewUnits.setText(R.string.text_unit_mgxdl); + } + TextView textViewTrend = (TextView) findViewById(R.id.textview_trend); + TextView textViewIOB = (TextView) findViewById(R.id.textview_iob); + //LineChart chart = (LineChart) findViewById(R.id.chart); + + // Get the most recently written CGM record. + RealmResults<CgmStatusEvent> results = + mRealm.where(CgmStatusEvent.class) + .findAllSorted("eventDate", Sort.ASCENDING); - // FIXME - replace initial polling time with the next expected polling period - // i.e. Next 5 minute CGM polling increment to occur after now, + POLL_GRACE_PERIOD_MS - startCgmServicePolling(System.currentTimeMillis() + MedtronicCnlIntentService.POLL_PERIOD_MS); + CgmStatusEvent cgmRecord = null; + + if (results.size() > 0) { + cgmRecord = results.last(); + } + + if (cgmRecord == null) { + return; + } DecimalFormat df; if (prefs.getBoolean("mmolDecimals", false)) @@ -413,22 +488,88 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc String sgvString, units; if (prefs.getBoolean("mmolxl", false)) { - float fBgValue = (float) cgmRecord.sgv; + float fBgValue = (float) cgmRecord.getSgv(); sgvString = df.format(fBgValue / 18.016f); units = "mmol/L"; - log.info("mmolxl true --> " + sgvString); + Log.d(TAG, "mmolxl true --> " + sgvString); } else { - sgvString = String.valueOf(cgmRecord.sgv); + sgvString = String.valueOf(cgmRecord.getSgv()); units = "mg/dL"; - log.info("mmolxl false --> " + sgvString); + Log.d(TAG, "mmolxl false --> " + sgvString); } - mTextViewBg.setText(sgvString); - mTextViewUnits.setText(units); - mTextViewBgTime.setText(DateUtils.formatDateTime(getBaseContext(), cgmRecord.sgvDate.getTime(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME)); - mTextViewTrend.setText(Html.fromHtml(renderTrendHtml(cgmRecord.getTrend()))); - mTextViewIOB.setText(String.format(Locale.getDefault(), "%.2f", pumpStatusRecord.activeInsulin)); + textViewBg.setText(sgvString); + textViewUnits.setText(units); + textViewBgTime.setText(DateUtils.getRelativeTimeSpanString(cgmRecord.getEventDate().getTime())); + textViewTrend.setText(Html.fromHtml(renderTrendHtml(cgmRecord.getTrend()))); + textViewIOB.setText(String.format(Locale.getDefault(), "%.2f", pumpStatusRecord.activeInsulin)); + + // TODO - waiting for MPAndroidCharts 3.0.0. This will fix: + // Date support + // Realm v1.0.0 support + // updateChart(results); + + // Run myself again in 60 seconds; + mUiRefreshHandler.postDelayed(this, 60000L); + } + + private void updateChart(RealmResults<CgmStatusEvent> results) { + RealmLineDataSet<CgmStatusEvent> lineDataSet = new RealmLineDataSet<>(results, "sgv", "eventDate"); + + lineDataSet.setDrawCircleHole(false); + lineDataSet.setColor(ColorTemplate.rgb("#FF5722")); + lineDataSet.setCircleColor(ColorTemplate.rgb("#FF5722")); + lineDataSet.setLineWidth(1.8f); + lineDataSet.setCircleSize(3.6f); + + ArrayList<ILineDataSet> dataSets = new ArrayList<ILineDataSet>(); + dataSets.add(lineDataSet); + + RealmLineData lineData = new RealmLineData(results, "eventDate", dataSets); + + // set data + //chart.setMinimumHeight(200); + //chart.setData(lineData); + } + } + + private class RefreshDataReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + CgmStatusEvent record = mRealm.where(CgmStatusEvent.class) + .findAll() + .last(); + + long nextPoll = record.getEventDate().getTime() + MedtronicCnlIntentService.POLL_GRACE_PERIOD_MS + MedtronicCnlIntentService.POLL_PERIOD_MS; + startCgmServicePolling(nextPoll); + Log.d(TAG, "Next Poll at " + new Date(nextPoll).toString()); + + // Delete invalid or old records from Realm + // TODO - show an error message if the valid records haven't been uploaded + final RealmResults<CgmStatusEvent> results = + mRealm.where(CgmStatusEvent.class) + .equalTo("sgv", 0) + .or() + .lessThan("eventDate", new Date(System.currentTimeMillis() - (24 * 60 * 60 * 1000))) + .findAll(); + + if (results.size() > 0) { + mRealm.executeTransaction(new Realm.Transaction() { + @Override + public void execute(Realm realm) { + // Delete all matches + Log.d(TAG, "Deleting " + results.size() + " records from realm"); + results.deleteAllFromRealm(); + } + }); + } + + // TODO - handle isOffline in NightscoutUploadIntentService? + uploadCgmData(); + + refreshDisplay(); } } @@ -436,13 +577,36 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); - if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) { - mTextViewLog.setText(mTextViewLog.getText() + "\nUSB plugged in", BufferType.EDITABLE); - startCgmService(); + if (MedtronicCnlIntentService.Constants.ACTION_USB_PERMISSION.equals(action)) { + boolean permissionGranted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false); + if (permissionGranted) { + Log.d(TAG, "Got permission to access USB"); + startCgmService(); + } else { + Log.d(TAG, "Still no permission for USB. Waiting..."); + waitForUsbPermission(); + } + } else if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) { + Log.d(TAG, "USB plugged in"); + + if (hasUsbPermission()) { + // Give the USB a little time to warm up first + startCgmService(System.currentTimeMillis() + MedtronicCnlIntentService.USB_WARMUP_TIME_MS); + } else { + Log.d(TAG, "No permission for USB. Waiting."); + waitForUsbPermission(); + } } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) { - mTextViewLog.setText(mTextViewLog.getText() + "\nUSB unplugged", BufferType.EDITABLE); + Log.d(TAG, "USB unplugged"); + if (mEnableCgmService) { + showDisconnectionNotification("USB Error", "Contour Next Link unplugged."); + } + } else if (MedtronicCnlIntentService.Constants.ACTION_NO_USB_PERMISSION.equals(action)) { + Log.d(TAG, "No permission to read the USB device."); + requestUsbPermission(); + } else if (MedtronicCnlIntentService.Constants.ACTION_USB_REGISTER.equals(action)) { + openUsbRegistration(); } - } } @@ -452,7 +616,6 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc if (arg1.getAction().equalsIgnoreCase(Intent.ACTION_BATTERY_LOW) || arg1.getAction().equalsIgnoreCase(Intent.ACTION_BATTERY_CHANGED) || arg1.getAction().equalsIgnoreCase(Intent.ACTION_BATTERY_OKAY)) { - Log.i("BatteryReceived", "BatteryReceived"); batLevel = arg1.getIntExtra(BatteryManager.EXTRA_LEVEL, 0); } } diff --git a/app/src/main/java/info/nightscout/android/medtronic/MedtronicCNLReader.java b/app/src/main/java/info/nightscout/android/medtronic/MedtronicCNLReader.java index 94658af861e75b153215d42d197e9ff1ca1fab26..8b56ea37e359278c03f25173894a2bd12f9b5ea0 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/MedtronicCNLReader.java +++ b/app/src/main/java/info/nightscout/android/medtronic/MedtronicCNLReader.java @@ -32,7 +32,7 @@ import info.nightscout.android.medtronic.message.PumpTimeResponseMessage; import info.nightscout.android.medtronic.message.ReadInfoResponseMessage; import info.nightscout.android.medtronic.message.UnexpectedMessageException; import info.nightscout.android.medtronic.service.MedtronicCnlIntentService; -import info.nightscout.android.upload.MedtronicNG.CGMRecord; +import info.nightscout.android.model.CgmStatusEvent; import info.nightscout.android.utils.HexDump; /** @@ -182,6 +182,27 @@ public class MedtronicCNLReader implements ContourNextLinkMessageHandler { } } + private static CgmStatusEvent.TREND fromMessageByte(byte messageByte) { + switch( messageByte ) { + case (byte) 0x60: + return CgmStatusEvent.TREND.FLAT; + case (byte) 0xc0: + return CgmStatusEvent.TREND.DOUBLE_UP; + case (byte) 0xa0: + return CgmStatusEvent.TREND.SINGLE_UP; + case (byte) 0x80: + return CgmStatusEvent.TREND.FOURTY_FIVE_UP; + case (byte) 0x40: + return CgmStatusEvent.TREND.FOURTY_FIVE_DOWN; + case (byte) 0x20: + return CgmStatusEvent.TREND.SINGLE_DOWN; + case (byte) 0x00: + return CgmStatusEvent.TREND.DOUBLE_DOWN; + default: + return CgmStatusEvent.TREND.NOT_COMPUTABLE; + } + } + public void enterControlMode() throws IOException, TimeoutException, UnexpectedMessageException { boolean doRetry = false; @@ -290,7 +311,7 @@ public class MedtronicCNLReader implements ContourNextLinkMessageHandler { return MessageUtils.decodeDateTime(rtc, offset); } - public void getPumpStatus(CGMRecord cgmRecord) throws IOException, EncryptionException, ChecksumException, TimeoutException { + public void getPumpStatus(CgmStatusEvent cgmRecord, long pumpTimeOffset) throws IOException, EncryptionException, ChecksumException, TimeoutException { // FIXME - throw if not in EHSM mode (add a state machine) new PumpStatusRequestMessage(mPumpSession).send(this); @@ -313,21 +334,21 @@ public class MedtronicCNLReader implements ContourNextLinkMessageHandler { // Read the data into the record long rawActiveInsulin = statusBuffer.getShort(0x33) & 0x0000ffff; MainActivity.pumpStatusRecord.activeInsulin = new BigDecimal(rawActiveInsulin / 10000f).setScale(3, BigDecimal.ROUND_HALF_UP); - cgmRecord.sgv = statusBuffer.getShort(0x35) & 0x0000ffff; // In mg/DL. 0 means no CGM reading + cgmRecord.setSgv(statusBuffer.getShort(0x35) & 0x0000ffff); // In mg/DL. 0 means no CGM reading long rtc; long offset; - if ((cgmRecord.sgv & 0x200) == 0x200) { + if ((cgmRecord.getSgv() & 0x200) == 0x200) { // Sensor error. Let's reset. FIXME - solve this more elegantly later - cgmRecord.sgv = 0; + cgmRecord.setSgv(0); rtc = 0; offset = 0; - cgmRecord.setTrend(CGMRecord.TREND.NOT_SET); + cgmRecord.setTrend(CgmStatusEvent.TREND.NOT_SET); } else { rtc = statusBuffer.getInt(0x37) & 0x00000000ffffffffL; offset = statusBuffer.getInt(0x3b); - cgmRecord.setTrend(CGMRecord.fromMessageByte(statusBuffer.get(0x40))); + cgmRecord.setTrend(fromMessageByte(statusBuffer.get(0x40))); } - cgmRecord.sgvDate = MessageUtils.decodeDateTime(rtc, offset); + cgmRecord.setEventDate(new Date(MessageUtils.decodeDateTime(rtc, offset).getTime() - pumpTimeOffset)); MainActivity.pumpStatusRecord.recentBolusWizard = statusBuffer.get(0x48) != 0; MainActivity.pumpStatusRecord.bolusWizardBGL = statusBuffer.getShort(0x49); // In mg/DL long rawReservoirAmount = statusBuffer.getInt(0x2b); diff --git a/app/src/main/java/info/nightscout/android/medtronic/StatusActivity.java b/app/src/main/java/info/nightscout/android/medtronic/StatusActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..2729d102872e24cf16c1540d69800104e78b7d3c --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/StatusActivity.java @@ -0,0 +1,45 @@ +package info.nightscout.android.medtronic; + +import android.graphics.Color; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.MenuItem; + +import com.mikepenz.google_material_typeface_library.GoogleMaterial; +import com.mikepenz.iconics.IconicsDrawable; + +import info.nightscout.android.R; + +public class StatusActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_status); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + + if (toolbar != null) { + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setHomeAsUpIndicator( + new IconicsDrawable(this) + .icon(GoogleMaterial.Icon.gmd_close) + .color(Color.WHITE) + .sizeDp(24) + ); + getSupportActionBar().setElevation(0); + getSupportActionBar().setTitle("Status"); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + } + + return super.onOptionsItemSelected(item); + } +} diff --git a/app/src/main/java/info/nightscout/android/medtronic/service/MedtronicCnlIntentService.java b/app/src/main/java/info/nightscout/android/medtronic/service/MedtronicCnlIntentService.java index bdab9b3c1c0124f55f182e7568da1f2209217da1..d39f3246dadb395b61d450476ec7e65335f8eca8 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/service/MedtronicCnlIntentService.java +++ b/app/src/main/java/info/nightscout/android/medtronic/service/MedtronicCnlIntentService.java @@ -4,24 +4,17 @@ import android.app.IntentService; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbManager; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; import android.support.v4.app.NotificationManagerCompat; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.Serializable; +import java.util.Date; import java.util.Locale; import java.util.concurrent.TimeoutException; -import info.nightscout.android.R; import info.nightscout.android.USB.UsbHidDriver; import info.nightscout.android.medtronic.MainActivity; import info.nightscout.android.medtronic.MedtronicCNLReader; @@ -29,15 +22,15 @@ import info.nightscout.android.medtronic.message.ChecksumException; import info.nightscout.android.medtronic.message.EncryptionException; import info.nightscout.android.medtronic.message.MessageUtils; import info.nightscout.android.medtronic.message.UnexpectedMessageException; +import info.nightscout.android.model.CgmStatusEvent; import info.nightscout.android.model.medtronicNg.ContourNextLinkInfo; -import info.nightscout.android.upload.MedtronicNG.CGMRecord; -import info.nightscout.android.upload.MedtronicNG.PumpStatusRecord; -import info.nightscout.android.upload.UploadHelper; import io.realm.Realm; +import io.realm.RealmResults; public class MedtronicCnlIntentService extends IntentService { public final static int USB_VID = 0x1a79; public final static int USB_PID = 0x6210; + public final static long USB_WARMUP_TIME_MS = 5000L; public final static long POLL_PERIOD_MS = 300000L; // Number of additional seconds to wait after the next expected CGM poll, so that we don't interfere with CGM radio comms. public final static long POLL_GRACE_PERIOD_MS = 30000L; @@ -51,12 +44,6 @@ public class MedtronicCnlIntentService extends IntentService { super(MedtronicCnlIntentService.class.getName()); } - public final class Constants { - public static final String ACTION_STATUS_MESSAGE = "info.nightscout.android.medtronic.service.STATUS_MESSAGE"; - public static final String ACTION_CGM_DATA = "info.nightscout.android.medtronic.service.CGM_DATA"; - public static final String EXTENDED_DATA = "info.nightscout.android.medtronic.service.DATA"; - } - protected void sendStatus(String message) { Intent localIntent = new Intent(Constants.ACTION_STATUS_MESSAGE) @@ -64,10 +51,9 @@ public class MedtronicCnlIntentService extends IntentService { LocalBroadcastManager.getInstance(this).sendBroadcast(localIntent); } - protected void sendCgmRecord(Serializable cgmRecord) { + protected void sendMessage(String action) { Intent localIntent = - new Intent(Constants.ACTION_CGM_DATA) - .putExtra(Constants.EXTENDED_DATA, cgmRecord); + new Intent(action); LocalBroadcastManager.getInstance(this).sendBroadcast(localIntent); } @@ -99,198 +85,174 @@ public class MedtronicCnlIntentService extends IntentService { } protected void onHandleIntent(Intent intent) { - Log.i(TAG, "onHandleIntent called"); + Log.d(TAG, "onHandleIntent called"); if (!hasUsbHostFeature()) { sendStatus("It appears that this device doesn't support USB OTG."); + Log.w(TAG, "Device does not support USB OTG"); + return; + } + + UsbDevice cnlStick = UsbHidDriver.getUsbDevice(mUsbManager, USB_VID, USB_PID); + if (cnlStick == null) { + sendStatus("USB connection error. Is the Bayer Contour Next Link plugged in?"); + Log.w(TAG, "USB connection error. Is the CNL plugged in?"); + return; + } + + if (!mUsbManager.hasPermission(UsbHidDriver.getUsbDevice(mUsbManager, USB_VID, USB_PID))) { + sendMessage(Constants.ACTION_NO_USB_PERMISSION); return; } + mHidDevice = UsbHidDriver.acquire(mUsbManager, cnlStick); - UploadHelper mUploader = new UploadHelper(getBaseContext()); - mHidDevice = UsbHidDriver.acquire(mUsbManager, USB_VID, USB_PID); Realm realm = Realm.getDefaultInstance(); - // Load the initial data to the display - CGMRecord cgmRecord = loadData(); - PumpStatusRecord pumpRecord = MainActivity.pumpStatusRecord; + try { + mHidDevice.open(); + } catch (Exception e) { + Log.e(TAG, "Unable to open serial device", e); + return; + } - sendCgmRecord(cgmRecord); + MedtronicCNLReader cnlReader = new MedtronicCNLReader(mHidDevice); - if (mHidDevice == null) { - String title = "USB connection error"; - String msg = "Is the Bayer Contour NextLink plugged in?"; - //showNotification(title, msg); - sendStatus(title + "\n" + msg); - } else { - try { - mHidDevice.open(); - } catch (Exception e) { - Log.e(TAG, "Unable to open serial device", e); - return; - } + realm.beginTransaction(); - // Go get the data - MedtronicCNLReader cnlReader = new MedtronicCNLReader(mHidDevice); + try { + sendStatus("Connecting to the Contour Next Link..."); + Log.d(TAG, "Connecting to the Contour Next Link."); + cnlReader.requestDeviceInfo(); - try { - sendStatus("Connecting to the Contour Next Link..."); - cnlReader.requestDeviceInfo(); + // Is the device already configured? + ContourNextLinkInfo info = realm + .where(ContourNextLinkInfo.class) + .equalTo("serialNumber", cnlReader.getStickSerial()) + .findFirst(); - // Is the device already configured? - ContourNextLinkInfo info = realm - .where(ContourNextLinkInfo.class).equalTo("serialNumber", cnlReader.getStickSerial()) - .findFirst(); + if (info == null) { + info = new ContourNextLinkInfo(); + info.setSerialNumber(cnlReader.getStickSerial()); - if (info == null) { - info = new ContourNextLinkInfo(); - info.setSerialNumber(cnlReader.getStickSerial()); + info = realm.copyToRealm(info); + } - realm.beginTransaction(); - info = realm.copyToRealm(info); - realm.commitTransaction(); - } + String hmac = info.getHmac(); + String key = info.getKey(); + + if (hmac == null || key == null) { + // Must commit the transaction before we send the Registration activation message + realm.commitTransaction(); - String hmac = info.getHmac(); - String key = info.getKey(); + sendMessage(Constants.ACTION_USB_REGISTER); + return; + } - String deviceName = String.format("medtronic-640g://%s", cnlReader.getStickSerial()); - cgmRecord.setDeviceName(deviceName); - pumpRecord.setDeviceName(deviceName); + cnlReader.getPumpSession().setHMAC(MessageUtils.hexStringToByteArray(hmac)); + cnlReader.getPumpSession().setKey(MessageUtils.hexStringToByteArray(key)); - if (hmac == null || key == null) { - sendStatus(String.format("Before you can use the Contour Next Link, you need to register it with the app. Select '%s' from the menu.", getString(R.string.register_contour_next_link))); - return; - } + cnlReader.enterControlMode(); + + try { + cnlReader.enterPassthroughMode(); + cnlReader.openConnection(); + cnlReader.requestReadInfo(); + byte radioChannel = cnlReader.negotiateChannel(); + if (radioChannel == 0) { + sendStatus("Could not communicate with the 640g. Are you near the pump?"); + Log.i(TAG, "Could not communicate with the 640g. Are you near the pump?"); + } else { + sendStatus(String.format(Locale.getDefault(), "Connected to Contour Next Link on channel %d.", (int) radioChannel)); + Log.d(TAG, String.format("Connected to Contour Next Link on channel %d.", (int) radioChannel)); + cnlReader.beginEHSMSession(); + + //PumpStatusEvent pumpRecord = realm.createObject(PumpStatusEvent.class); + CgmStatusEvent cgmRecord = realm.createObject(CgmStatusEvent.class); + + String deviceName = String.format("medtronic-640g://%s", cnlReader.getStickSerial()); + cgmRecord.setDeviceName(deviceName); + //pumpRecord.setDeviceName(deviceName); + // TODO - legacy. Remove once we've plumbed in pumpRecord. + MainActivity.pumpStatusRecord.setDeviceName(deviceName); + + //pumpRecord.setPumpDate(cnlReader.getPumpTime()); + long pumpTime = cnlReader.getPumpTime().getTime(); + long pumpOffset = pumpTime - System.currentTimeMillis(); + // TODO - send ACTION to MainActivity to show offset between pump and uploader. + MainActivity.pumpStatusRecord.pumpDate = new Date(pumpTime - pumpOffset); + cnlReader.getPumpStatus(cgmRecord, pumpOffset); + + cnlReader.endEHSMSession(); + + boolean cancelTransaction = true; + if (cgmRecord.getSgv() != 0) { + // Check that the record doesn't already exist before committing + RealmResults<CgmStatusEvent> checkExistingRecords = realm + .where(CgmStatusEvent.class) + .equalTo("eventDate", cgmRecord.getEventDate()) + .equalTo("deviceName", cgmRecord.getDeviceName()) + .equalTo("sgv", cgmRecord.getSgv()) + .findAll(); + + // There should be the 1 record we've already added in this transaction. + if (checkExistingRecords.size() <= 1) { + realm.commitTransaction(); + cancelTransaction = false; + } + + // Tell the Main Activity we have new data + sendMessage(Constants.ACTION_REFRESH_DATA); + } - cnlReader.getPumpSession().setHMAC(MessageUtils.hexStringToByteArray(hmac)); - cnlReader.getPumpSession().setKey(MessageUtils.hexStringToByteArray(key)); - - cnlReader.enterControlMode(); - try { - cnlReader.enterPassthroughMode(); - cnlReader.openConnection(); - cnlReader.requestReadInfo(); - byte radioChannel = cnlReader.negotiateChannel(); - if (radioChannel == 0) { - sendStatus("Could not communicate with the 640g. Are you near the pump?"); - Log.i(TAG, "Could not communicate with the 640g. Are you near the pump?"); - } else { - sendStatus(String.format(Locale.getDefault(), "Connected to Contour Next Link on channel %d.", (int) radioChannel)); - Log.d(TAG, String.format("Connected to Contour Next Link on channel %d.", (int) radioChannel)); - cnlReader.beginEHSMSession(); - - pumpRecord.pumpDate = cnlReader.getPumpTime(); - cnlReader.getPumpStatus(cgmRecord); - - writeData(cgmRecord); - sendCgmRecord(cgmRecord); - cnlReader.endEHSMSession(); + if (cancelTransaction) { + realm.cancelTransaction(); } - cnlReader.closeConnection(); - } catch (UnexpectedMessageException e) { - Log.e(TAG, "Unexpected Message", e); - sendStatus("Communication Error: " + e.getMessage()); - - } finally { - cnlReader.endPassthroughMode(); - cnlReader.endControlMode(); } - } catch (IOException e) { - Log.e(TAG, "Error getting SGVs", e); - sendStatus("Error connecting to Contour Next Link."); - } catch (ChecksumException e) { - Log.e(TAG, "Checksum error", e); - sendStatus("Checksum error getting message from the Contour Next Link."); - } catch (EncryptionException e) { - Log.e(TAG, "Encryption exception", e); - sendStatus("Error decrypting messages from Contour Next Link."); - } catch (TimeoutException e) { - Log.e(TAG, "Timeout communicating with Contour", e); - sendStatus("Timeout communicating with the Contour Next Link."); + + cnlReader.closeConnection(); } catch (UnexpectedMessageException e) { Log.e(TAG, "Unexpected Message", e); - sendStatus("Could not close connection: " + e.getMessage()); + sendStatus("Communication Error: " + e.getMessage()); + } finally { + cnlReader.endPassthroughMode(); + cnlReader.endControlMode(); } - - // TODO - add retries. - if (!isOnline()) { - String title = "Cannot upload data"; - String msg = "Please check that you're connected to the Internet"; - //showNotification(title, msg); - sendStatus(title + "\n" + msg); - } else { - // FIXME - DO THE UPLOAD! - //mUploader.execute(cgmRecord); + } catch (IOException e) { + Log.e(TAG, "Error connecting to Contour Next Link.", e); + sendStatus("Error connecting to Contour Next Link."); + } catch (ChecksumException e) { + Log.e(TAG, "Checksum error getting message from the Contour Next Link.", e); + sendStatus("Checksum error getting message from the Contour Next Link."); + } catch (EncryptionException e) { + Log.e(TAG, "Error decrypting messages from Contour Next Link.", e); + sendStatus("Error decrypting messages from Contour Next Link."); + } catch (TimeoutException e) { + Log.e(TAG, "Timeout communicating with the Contour Next Link.", e); + sendStatus("Timeout communicating with the Contour Next Link."); + } catch (UnexpectedMessageException e) { + Log.e(TAG, "Could not close connection.", e); + sendStatus("Could not close connection: " + e.getMessage()); + } finally { + if (realm.isInTransaction()) { + // If we didn't commit the transaction, we've run into an error. Let's roll it back + realm.cancelTransaction(); } - - realm.close(); } - } - private boolean isOnline() { - ConnectivityManager cm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo netInfo = cm.getActiveNetworkInfo(); - return netInfo != null && netInfo.isConnectedOrConnecting(); + realm.close(); } private boolean hasUsbHostFeature() { return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_USB_HOST); } - // FIXME - replace this with writing to the SQLite DB. - private void writeData(CGMRecord cgmRecord) { - //Write most recent data - try { - Context context = getBaseContext(); - ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File(context.getFilesDir(), "save.bin"))); //Select where you wish to save the file... - oos.writeObject(cgmRecord); // write the class as an 'object' - oos.flush(); // flush the stream to insure all of the information was written to 'save.bin' - oos.close();// close the stream - } catch (Exception e) { - Log.e(TAG, "write to OutputStream failed", e); - } - } - - /* - // FIXME - when we want to enable notifications, start with this. We'll need to fix the icon to match - // the Android standards (linter will fail anyway) - private void showNotification(String title, String message) { - NotificationManagerCompat nm = NotificationManagerCompat.from(mContext); - - // The PendingIntent to launch our activity if the user selects this notification - PendingIntent contentIntent = PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0); - nm.notify(R.string.app_name, new NotificationCompat.Builder(mContext) - .setDefaults(NotificationCompat.DEFAULT_ALL) - .setContentTitle(title) - .setStyle(new NotificationCompat.BigTextStyle() - .bigText(message)) - .setContentText(message) - .setTicker(message) - //.setSmallIcon(R.drawable.ic_action_warning) - .setSmallIcon(R.drawable.ic_launcher) - .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher)) - .setContentIntent(contentIntent) - .build()); - } - */ + public final class Constants { + public static final String ACTION_STATUS_MESSAGE = "info.nightscout.android.medtronic.service.STATUS_MESSAGE"; + public static final String ACTION_NO_USB_PERMISSION = "info.nightscout.android.medtronic.service.NO_USB_PERMISSION"; + public static final String ACTION_USB_PERMISSION = "info.nightscout.android.medtronic.USB_PERMISSION"; + public static final String ACTION_REFRESH_DATA = "info.nightscout.android.medtronic.service.CGM_DATA"; + public static final String ACTION_USB_REGISTER = "info.nightscout.android.medtronic.USB_REGISTER"; - private CGMRecord loadData() { - ObjectInputStream ois = null; - try { - Context context = getBaseContext(); - ois = new ObjectInputStream(new FileInputStream(new File(context.getFilesDir(), "save.bin"))); - Object o = ois.readObject(); - ois.close(); - return (CGMRecord) o; - } catch (Exception ex) { - Log.w(TAG, " unable to load Medtronic640g data"); - try { - if (ois != null) - ois.close(); - } catch (Exception e) { - Log.e(TAG, " Error closing ObjectInputStream"); - } - } - return new CGMRecord(); + public static final String EXTENDED_DATA = "info.nightscout.android.medtronic.service.DATA"; } } diff --git a/app/src/main/java/info/nightscout/android/model/CgmStatusEvent.java b/app/src/main/java/info/nightscout/android/model/CgmStatusEvent.java index c8759eeb61102ddd8c9b0dc5d24c04050d6b7a56..1100d3270879dfb38dd38311cde16f46235899b9 100644 --- a/app/src/main/java/info/nightscout/android/model/CgmStatusEvent.java +++ b/app/src/main/java/info/nightscout/android/model/CgmStatusEvent.java @@ -9,25 +9,14 @@ import io.realm.annotations.Index; * Created by lgoedhart on 4/06/2016. */ public class CgmStatusEvent extends RealmObject { - public enum TREND { - NONE, - DOUBLE_UP, - SINGLE_UP, - FOURTY_FIVE_UP, - FLAT, - FOURTY_FIVE_DOWN, - SINGLE_DOWN, - DOUBLE_DOWN, - NOT_COMPUTABLE, - RATE_OUT_OF_RANGE, - NOT_SET - } - @Index private Date eventDate; // The actual time of the event (assume the capture device eventDate/time is accurate) private Date pumpDate; // The eventDate/time on the pump at the time of the event + private String deviceName; private int sgv; private String trend; + @Index + private boolean uploaded = false; public Date getEventDate() { return eventDate; @@ -45,6 +34,14 @@ public class CgmStatusEvent extends RealmObject { this.pumpDate = pumpDate; } + public String getDeviceName() { + return deviceName; + } + + public void setDeviceName(String deviceName) { + this.deviceName = deviceName; + } + public int getSgv() { return sgv; } @@ -60,4 +57,26 @@ public class CgmStatusEvent extends RealmObject { public void setTrend(TREND trend) { this.trend = trend.name(); } + + public boolean isUploaded() { + return uploaded; + } + + public void setUploaded(boolean uploaded) { + this.uploaded = uploaded; + } + + public enum TREND { + NONE, + DOUBLE_UP, + SINGLE_UP, + FOURTY_FIVE_UP, + FLAT, + FOURTY_FIVE_DOWN, + SINGLE_DOWN, + DOUBLE_DOWN, + NOT_COMPUTABLE, + RATE_OUT_OF_RANGE, + NOT_SET + } } diff --git a/app/src/main/java/info/nightscout/android/model/medtronicNg/PumpStatusEvent.java b/app/src/main/java/info/nightscout/android/model/medtronicNg/PumpStatusEvent.java index 741f1a68891a94ba73e1a374bfff69e2a953c3c6..1a11543456646e2b9040966c3b545085c1f6e704 100644 --- a/app/src/main/java/info/nightscout/android/model/medtronicNg/PumpStatusEvent.java +++ b/app/src/main/java/info/nightscout/android/model/medtronicNg/PumpStatusEvent.java @@ -12,8 +12,65 @@ public class PumpStatusEvent extends RealmObject { @Index private Date eventDate; // The actual time of the event (assume the capture device eventDate/time is accurate) private Date pumpDate; // The eventDate/time on the pump at the time of the event + private String deviceName; private float activeInsulin; private float reservoirAmount; private boolean recentBolusWizard; // Whether a bolus wizard has been run recently private int bolusWizardBGL; // in mg/dL. 0 means no recent bolus wizard reading. + + public Date getEventDate() { + return eventDate; + } + + public void setEventDate(Date eventDate) { + this.eventDate = eventDate; + } + + public Date getPumpDate() { + return pumpDate; + } + + public void setPumpDate(Date pumpDate) { + this.pumpDate = pumpDate; + } + + public String getDeviceName() { + return deviceName; + } + + public void setDeviceName(String deviceName) { + this.deviceName = deviceName; + } + + public float getActiveInsulin() { + return activeInsulin; + } + + public void setActiveInsulin(float activeInsulin) { + this.activeInsulin = activeInsulin; + } + + public float getReservoirAmount() { + return reservoirAmount; + } + + public void setReservoirAmount(float reservoirAmount) { + this.reservoirAmount = reservoirAmount; + } + + public boolean isRecentBolusWizard() { + return recentBolusWizard; + } + + public void setRecentBolusWizard(boolean recentBolusWizard) { + this.recentBolusWizard = recentBolusWizard; + } + + public int getBolusWizardBGL() { + return bolusWizardBGL; + } + + public void setBolusWizardBGL(int bolusWizardBGL) { + this.bolusWizardBGL = bolusWizardBGL; + } } diff --git a/app/src/main/java/info/nightscout/android/settings/SettingsFragment.java b/app/src/main/java/info/nightscout/android/settings/SettingsFragment.java index 7519a5c7f75cc4e177012ca37b2ec08d272d0b8a..e070ac229cd308a3e7f82ae50528aa789bfcbb21 100644 --- a/app/src/main/java/info/nightscout/android/settings/SettingsFragment.java +++ b/app/src/main/java/info/nightscout/android/settings/SettingsFragment.java @@ -10,7 +10,13 @@ import android.preference.Preference; import android.preference.PreferenceCategory; import android.preference.PreferenceFragment; +import java.io.IOException; + import info.nightscout.android.R; +import info.nightscout.android.upload.nightscout.NightscoutApi; +import retrofit2.Call; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; public class SettingsFragment extends PreferenceFragment implements OnSharedPreferenceChangeListener { diff --git a/app/src/main/java/info/nightscout/android/upload/UploadHelper.java b/app/src/main/java/info/nightscout/android/upload/UploadHelper.java deleted file mode 100644 index d6c06f609a458b297eb2df36aeb33c29fed4ee2a..0000000000000000000000000000000000000000 --- a/app/src/main/java/info/nightscout/android/upload/UploadHelper.java +++ /dev/null @@ -1,478 +0,0 @@ -package info.nightscout.android.upload; - -import android.content.Context; -import android.content.SharedPreferences; -import android.os.AsyncTask; -import android.preference.PreferenceManager; -import android.util.Log; - -import org.apache.http.client.ResponseHandler; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.BasicResponseHandler; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.params.BasicHttpParams; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; -import org.json.JSONArray; -import org.json.JSONObject; -import org.slf4j.LoggerFactory; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.security.MessageDigest; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import ch.qos.logback.classic.Logger; -import info.nightscout.android.R; -import info.nightscout.android.medtronic.MainActivity; -import info.nightscout.android.medtronic.MedtronicConstants; -import info.nightscout.android.upload.MedtronicNG.CGMRecord; - -public class UploadHelper extends AsyncTask<Record, Integer, Long> { - - private Logger log = (Logger) LoggerFactory.getLogger(UploadHelper.class.getName()); - private static final String TAG = "DexcomUploadHelper"; - private SharedPreferences settings = null;// common application preferences - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("MM/dd/yyyy hh:mm:ss aa", Locale.getDefault()); - private static final int SOCKET_TIMEOUT = 60 * 1000; - private static final int CONNECTION_TIMEOUT = 30 * 1000; - - Context context; - private List<JSONObject> recordsNotUploadedList = new ArrayList<JSONObject>(); - private List<JSONObject> recordsNotUploadedListJson = new ArrayList<JSONObject>(); - - public static final Object isModifyingRecordsLock = new Object(); - - public UploadHelper(Context context) { - this.context = context; - settings = context.getSharedPreferences(MedtronicConstants.PREFS_NAME, 0); - synchronized (isModifyingRecordsLock) { - try { - long currentTime = System.currentTimeMillis(); - long diff = currentTime - settings.getLong("lastDestroy", 0); - if (diff != currentTime && diff > (6*MedtronicConstants.TIME_60_MIN_IN_MS)) { - log.debug("Remove older records"); - SharedPreferences.Editor editor = settings.edit(); - if (settings.contains("recordsNotUploaded")) - editor.remove("recordsNotUploaded"); - if (settings.contains("recordsNotUploadedJson")) - editor.remove("recordsNotUploadedJson"); - editor.apply(); - } - if (settings.contains("recordsNotUploaded")){ - JSONArray recordsNotUploaded = new JSONArray(settings.getString("recordsNotUploaded","[]")); - for (int i = 0; i < recordsNotUploaded.length(); i++){ - recordsNotUploadedList.add(recordsNotUploaded.getJSONObject(i)); - } - log.debug("retrieve older json records -->" +recordsNotUploaded.length()); - SharedPreferences.Editor editor = settings.edit(); - editor.remove("recordsNotUploaded"); - editor.apply(); - } - if (settings.contains("recordsNotUploadedJson")){ - JSONArray recordsNotUploadedJson = new JSONArray(settings.getString("recordsNotUploadedJson","[]")); - for (int i = 0; i < recordsNotUploadedJson.length(); i++){ - recordsNotUploadedListJson.add(recordsNotUploadedJson.getJSONObject(i)); - } - log.debug("retrieve older json records -->" +recordsNotUploadedJson.length()); - SharedPreferences.Editor editor = settings.edit(); - editor.remove("recordsNotUploadedJson"); - editor.apply(); - } - } catch (Exception e) { - log.debug("ERROR Retrieving older list, I have lost them"); - recordsNotUploadedList = new ArrayList<>(); - recordsNotUploadedListJson = new ArrayList<>(); - SharedPreferences.Editor editor = settings.edit(); - if (settings.contains("recordsNotUploaded")) - editor.remove("recordsNotUploaded"); - if (settings.contains("recordsNotUploadedJson")) - editor.remove("recordsNotUploadedJson"); - editor.apply(); - } - } - } - - /** - * doInBackground - */ - protected Long doInBackground(Record... records) { - - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this.context); - Boolean enableRESTUpload = prefs.getBoolean("EnableRESTUpload", false); - try{ - if (enableRESTUpload) { - long start = System.currentTimeMillis(); - Log.i(TAG, String.format("Starting upload of %s record using a REST API", records.length)); - log.info(String.format("Starting upload of %s record using a REST API", records.length)); - doRESTUpload(prefs, records); - Log.i(TAG, String.format("Finished upload of %s record using a REST API in %s ms", records.length, System.currentTimeMillis() - start)); - log.info(String.format("Finished upload of %s record using a REST API in %s ms", records.length, System.currentTimeMillis() - start)); - } - }catch(Exception e){ - log.error("ERROR uploading data!!!!!", e); - } - - - return 1L; - } - - protected void onPostExecute(Long result) { - super.onPostExecute(result); - Log.i(TAG, "Post execute, Result: " + result + ", Status: FINISHED"); - log.info("Post execute, Result: " + result + ", Status: FINISHED"); - - } - - private void doRESTUpload(SharedPreferences prefs, Record... records) { - String apiScheme = "https://"; - String apiUrl = ""; - String apiSecret = prefs.getString(context.getString(R.string.preference_api_secret), "YOURAPISECRET"); - - // Add the extra match for "KEY@" to support the previous single field - Pattern p = Pattern.compile("(.*\\/\\/)?(.*@)?([^\\/]*)(.*)"); - Matcher m = p.matcher(prefs.getString(context.getString(R.string.preference_nightscout_url), "")); - - if( m.find() ) { - apiUrl = m.group(3); - - // Only override apiSecret from URL (the "old" way), if the API secret preference is empty - if( apiSecret.equals("YOURAPISECRET") || apiSecret.equals("") ) { - apiSecret = ( m.group(2) == null ) ? "" : m.group(2).replace("@", ""); - } - - // Override the URI scheme if it's been provided in the preference) - if( m.group(1) != null && !m.group(1).equals("") ) { - apiScheme = m.group(1); - } - } - - // Update the preferences to match what we expect. Only really used from converting from the - // old format to the new format. Aren't we nice for managing backward compatibility? - prefs.edit().putString(context.getString(R.string.preference_api_secret), apiSecret ).apply(); - prefs.edit().putString(context.getString(R.string.preference_nightscout_url), String.format("%s%s", apiScheme, apiUrl ) ).apply(); - - String uploadUrl = String.format("%s%s@%s/api/v1/", apiScheme, apiSecret, apiUrl ); - - try { - doRESTUploadTo(uploadUrl, records); - } catch (Exception e) { - Log.e(TAG, "Unable to do REST API Upload to: " + uploadUrl, e); - log.error("Unable to do REST API Upload to: " + uploadUrl, e); - } - } - - private void doRESTUploadTo(String baseURI, Record[] records) { - try { - int apiVersion = 0; - if (baseURI.endsWith("/v1/")) apiVersion = 1; - - String baseURL; - String secret = null; - String[] uriParts = baseURI.split("@"); - - if (uriParts.length == 1 && apiVersion == 0) { - baseURL = uriParts[0]; - } else if (uriParts.length == 1) { - if (recordsNotUploadedListJson.size() > 0){ - JSONArray jsonArray = new JSONArray(recordsNotUploadedListJson); - SharedPreferences.Editor editor = settings.edit(); - editor.putString("recordsNotUploaded", jsonArray.toString()); - editor.apply(); - } - throw new Exception("Starting with API v1, a pass phase is required"); - } else if (uriParts.length == 2 && apiVersion > 0) { - secret = uriParts[0]; - baseURL = uriParts[1]; - - // new format URL! - - if (secret.contains("http")) { - if (secret.contains("https")) { - baseURL = "https://" + baseURL; - } else { - baseURL = "http://" + baseURL; - } - String[] uriParts2 = secret.split("//"); - secret = uriParts2[1]; - } - - - } else { - if (recordsNotUploadedListJson.size() > 0){ - JSONArray jsonArray = new JSONArray(recordsNotUploadedListJson); - SharedPreferences.Editor editor = settings.edit(); - editor.putString("recordsNotUploadedJson", jsonArray.toString()); - editor.apply(); - } - throw new Exception(String.format("Unexpected baseURI: %s, uriParts.length: %s, apiVersion: %s", baseURI, uriParts.length, apiVersion)); - } - - - - HttpParams params = new BasicHttpParams(); - HttpConnectionParams.setSoTimeout(params, SOCKET_TIMEOUT); - HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT); - - DefaultHttpClient httpclient = new DefaultHttpClient(params); - - postDeviceStatus(baseURL,httpclient); - - if (recordsNotUploadedListJson.size() > 0){ - List<JSONObject> auxList = new ArrayList<JSONObject>(recordsNotUploadedListJson); - recordsNotUploadedListJson.clear(); - for (int i = 0; i < auxList.size(); i++){ - JSONObject json = auxList.get(i); - String postURL = baseURL; - postURL += "entries"; - Log.i(TAG, "postURL: " + postURL); - - HttpPost post = new HttpPost(postURL); - - if (apiVersion > 0) { - if (secret == null || secret.isEmpty()) { - if (auxList.size() > 0){ - JSONArray jsonArray = new JSONArray(auxList); - SharedPreferences.Editor editor = settings.edit(); - editor.putString("recordsNotUploaded", jsonArray.toString()); - editor.apply(); - } - throw new Exception("Starting with API v1, a pass phase is required"); - } else { - MessageDigest digest = MessageDigest.getInstance("SHA-1"); - byte[] bytes = secret.getBytes("UTF-8"); - digest.update(bytes, 0, bytes.length); - bytes = digest.digest(); - StringBuilder sb = new StringBuilder(bytes.length * 2); - for (byte b: bytes) { - sb.append(String.format("%02x", b & 0xff)); - } - String token = sb.toString(); - post.setHeader("api-secret", token); - } - } - - String jsonString = json.toString(); - - Log.i(TAG, "Upload JSON: " + jsonString); - log.debug("JSON to Upload "+ jsonString); - - try { - StringEntity se = new StringEntity(jsonString); - post.setEntity(se); - post.setHeader("Accept", "application/json"); - post.setHeader("Content-type", "application/json"); - - ResponseHandler responseHandler = new BasicResponseHandler(); - httpclient.execute(post, responseHandler); - } catch (Exception e) { - Log.w(TAG, "Unable to post data to: '" + post.getURI().toString() + "'", e); - log.warn("Unable to post data to: '" + post.getURI().toString() + "'", e); - } - } - } - - for (Record record : records) { - String postURL = baseURL; - if (record instanceof GlucometerRecord){ - postURL += "entries"; - }else{ - postURL += "entries"; - } - Log.i(TAG, "postURL: " + postURL); - log.info( "postURL: " + postURL); - - HttpPost post = new HttpPost(postURL); - - if (apiVersion > 0) { - if (secret == null || secret.isEmpty()) { - if (recordsNotUploadedListJson.size() > 0){ - JSONArray jsonArray = new JSONArray(recordsNotUploadedListJson); - SharedPreferences.Editor editor = settings.edit(); - editor.putString("recordsNotUploadedJson", jsonArray.toString()); - editor.apply(); - } - throw new Exception("Starting with API v1, a pass phase is required"); - } else { - MessageDigest digest = MessageDigest.getInstance("SHA-1"); - byte[] bytes = secret.getBytes("UTF-8"); - digest.update(bytes, 0, bytes.length); - bytes = digest.digest(); - StringBuilder sb = new StringBuilder(bytes.length * 2); - for (byte b: bytes) { - sb.append(String.format("%02x", b & 0xff)); - } - String token = sb.toString(); - post.setHeader("api-secret", token); - } - } - - JSONObject json = new JSONObject(); - - try { - populateV1APIEntry(json, record); - } catch (Exception e) { - Log.w(TAG, "Unable to populate entry, apiVersion: " + apiVersion, e); - log.warn("Unable to populate entry, apiVersion: " + apiVersion, e); - continue; - } - - String jsonString = json.toString(); - - Log.i(TAG, "Upload JSON: " + jsonString); - log.info("Upload JSON: " + jsonString); - - try { - StringEntity se = new StringEntity(jsonString); - post.setEntity(se); - post.setHeader("Accept", "application/json"); - post.setHeader("Content-type", "application/json"); - - ResponseHandler responseHandler = new BasicResponseHandler(); - httpclient.execute(post, responseHandler); - } catch (Exception e) { - //Only EGV records are important enough. - if (recordsNotUploadedListJson.size() > 49){ - recordsNotUploadedListJson.remove(0); - recordsNotUploadedListJson.add(49,json); - }else{ - recordsNotUploadedListJson.add(json); - } - - Log.w(TAG, "Unable to post data to: '" + post.getURI().toString() + "'", e); - log.warn( "Unable to post data to: '" + post.getURI().toString() + "'", e); - } - } - } catch (Exception e) { - Log.e(TAG, "Unable to post data", e); - log.error("Unable to post data", e); - } - if (recordsNotUploadedListJson.size() > 0){ - synchronized (isModifyingRecordsLock) { - try { - JSONArray recordsNotUploadedJson = new JSONArray(settings.getString("recordsNotUploadedJson","[]")); - if (recordsNotUploadedJson.length() > 0 && recordsNotUploadedJson.length() < recordsNotUploadedListJson.size()){ - for (int i = 0; i < recordsNotUploadedJson.length(); i++){ - if (recordsNotUploadedListJson.size() > 49){ - recordsNotUploadedListJson.remove(0); - recordsNotUploadedListJson.add(49, recordsNotUploadedJson.getJSONObject(i)); - }else{ - recordsNotUploadedListJson.add(recordsNotUploadedJson.getJSONObject(i)); - } - } - }else{ - for (int i = 0; i < recordsNotUploadedListJson.size(); i++){ - recordsNotUploadedJson.put(recordsNotUploadedListJson.get(i)); - } - int start = 0; - if (recordsNotUploadedJson.length() > 50){ - start = recordsNotUploadedJson.length() - 51; - } - recordsNotUploadedListJson.clear(); - for (int i = start; i < recordsNotUploadedJson.length(); i++){ - recordsNotUploadedListJson.add(recordsNotUploadedJson.getJSONObject(i)); - } - } - log.debug("retrieve older json records -->" +recordsNotUploadedJson.length()); - SharedPreferences.Editor editor = settings.edit(); - editor.remove("recordsNotUploadedJson"); - editor.apply(); - } catch (Exception e) { - log.debug("ERROR RETRIEVING OLDER LISTs, I HAVE LOST THEM"); - SharedPreferences.Editor editor = settings.edit(); - if (settings.contains("recordsNotUploadedJson")) - editor.remove("recordsNotUploadedJson"); - editor.apply(); - } - JSONArray jsonArray = new JSONArray(recordsNotUploadedListJson); - SharedPreferences.Editor editor = settings.edit(); - editor.putString("recordsNotUploadedJson", jsonArray.toString()); - editor.apply(); - } - } - } - - private void postDeviceStatus(String baseURL, DefaultHttpClient httpclient) throws Exception { - String devicestatusURL = baseURL + "devicestatus"; - Log.i(TAG, "devicestatusURL: " + devicestatusURL); - log.info("devicestatusURL: " + devicestatusURL); - - JSONObject json = new JSONObject(); - json.put("uploaderBattery", MainActivity.batLevel); - json.put("device", MainActivity.pumpStatusRecord.getDeviceName() ); - - JSONObject pumpInfo = new JSONObject(); - pumpInfo.put( "clock", MainActivity.pumpStatusRecord.pumpDate ); - pumpInfo.put( "reservoir", MainActivity.pumpStatusRecord.reservoirAmount); - - JSONObject iob = new JSONObject(); - iob.put( "timestamp", MainActivity.pumpStatusRecord.pumpDate ); - iob.put( "bolusiob", MainActivity.pumpStatusRecord.activeInsulin ); - - JSONObject battery = new JSONObject(); - battery.put( "percent", MainActivity.pumpStatusRecord.batteryPercentage ); - - pumpInfo.put( "iob", iob ); - pumpInfo.put( "battery", battery ); - json.put( "pump", pumpInfo ); - String jsonString = json.toString(); - Log.i(TAG, "Device Status JSON: " + jsonString); - log.debug("Device Status JSON: "+ jsonString); - - HttpPost post = new HttpPost(devicestatusURL); - StringEntity se = new StringEntity(jsonString); - post.setEntity(se); - post.setHeader("Accept", "application/json"); - post.setHeader("Content-type", "application/json"); - - ResponseHandler responseHandler = new BasicResponseHandler(); - httpclient.execute(post, responseHandler); - } - - private void populateV1APIEntry(JSONObject json, Record oRecord) throws Exception { - if (oRecord instanceof CGMRecord) { - CGMRecord pumpRecord = (CGMRecord) oRecord; - json.put("sgv", pumpRecord.sgv); - json.put("direction", pumpRecord.direction); - json.put("device", pumpRecord.getDeviceName()); - json.put("type", "sgv"); - json.put("date", pumpRecord.sgvDate.getTime()); - json.put("dateString", pumpRecord.sgvDate); - } else { - Date date = DATE_FORMAT.parse(oRecord.displayTime); - json.put("date", date.getTime()); - } - } - - private static String convertStreamToString(InputStream is) { - - BufferedReader reader = new BufferedReader(new InputStreamReader(is)); - StringBuilder sb = new StringBuilder(); - - String line = null; - try { - while ((line = reader.readLine()) != null) { - sb.append(line).append("\n"); - } - } catch (IOException e) { - e.printStackTrace(); - } finally { - try { - is.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - return sb.toString(); - } - -} diff --git a/app/src/main/java/info/nightscout/android/upload/nightscout/NightScoutApi.java b/app/src/main/java/info/nightscout/android/upload/nightscout/NightScoutApi.java new file mode 100644 index 0000000000000000000000000000000000000000..c613dfbb5112dad61676c85cd04ebe520c9b56af --- /dev/null +++ b/app/src/main/java/info/nightscout/android/upload/nightscout/NightScoutApi.java @@ -0,0 +1,33 @@ +package info.nightscout.android.upload.nightscout; + +import retrofit2.Call; +import retrofit2.http.GET; + +/** + * Created by lgoedhart on 26/06/2016. + */ +public interface NightscoutApi { + class LoginStatus { + public final String status; + + public LoginStatus(String status) { + this.status = status; + } + } + + class SiteStatus { + public final String status; + public final String name; + + public SiteStatus(String status, String name) { + this.status = status; + this.name = name; + } + } + + @GET("/api/v1/status.json") + Call<SiteStatus> getStatus(); + + @GET("/api/v1/experiments/test") + Call<LoginStatus> testLogin(); +} diff --git a/app/src/main/java/info/nightscout/android/upload/nightscout/NightscoutUploadIntentService.java b/app/src/main/java/info/nightscout/android/upload/nightscout/NightscoutUploadIntentService.java new file mode 100644 index 0000000000000000000000000000000000000000..6ba4b5c8d65b8f71dfa44fcef38cadad02e4c87b --- /dev/null +++ b/app/src/main/java/info/nightscout/android/upload/nightscout/NightscoutUploadIntentService.java @@ -0,0 +1,289 @@ +package info.nightscout.android.upload.nightscout; + +import android.app.IntentService; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.preference.PreferenceManager; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; + +import org.apache.http.client.ResponseHandler; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.BasicResponseHandler; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; +import org.json.JSONObject; + +import java.security.MessageDigest; +import java.text.SimpleDateFormat; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import info.nightscout.android.R; +import info.nightscout.android.medtronic.MainActivity; +import info.nightscout.android.model.CgmStatusEvent; +import info.nightscout.android.upload.nightscout.serializer.EntriesSerializer; +import io.realm.Realm; +import io.realm.RealmResults; + +public class NightscoutUploadIntentService extends IntentService { + + private static final String TAG = NightscoutUploadIntentService.class.getSimpleName(); + private static final SimpleDateFormat ISO8601_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()); + private static final int SOCKET_TIMEOUT = 60 * 1000; + private static final int CONNECTION_TIMEOUT = 30 * 1000; + Context mContext; + private Realm mRealm; + + public NightscoutUploadIntentService() { + super(NightscoutUploadIntentService.class.getName()); + } + + protected void sendStatus(String message) { + Intent localIntent = + new Intent(Constants.ACTION_STATUS_MESSAGE) + .putExtra(Constants.EXTENDED_DATA, message); + LocalBroadcastManager.getInstance(this).sendBroadcast(localIntent); + } + + @Override + public void onCreate() { + super.onCreate(); + + Log.i(TAG, "onCreate called"); + mContext = this.getBaseContext(); + } + + @Override + protected void onHandleIntent(Intent intent) { + Log.d(TAG, "onHandleIntent called"); + mRealm = Realm.getDefaultInstance(); + + RealmResults<CgmStatusEvent> records = mRealm + .where(CgmStatusEvent.class) + .equalTo("uploaded", false) + .notEqualTo("sgv", 0) + .findAll(); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); + + Boolean enableRESTUpload = prefs.getBoolean("EnableRESTUpload", false); + try { + if (enableRESTUpload) { + long start = System.currentTimeMillis(); + Log.i(TAG, String.format("Starting upload of %s record using a REST API", records.size())); + doRESTUpload(prefs, records); + Log.i(TAG, String.format("Finished upload of %s record using a REST API in %s ms", records.size(), System.currentTimeMillis() - start)); + } + } catch (Exception e) { + Log.e(TAG, "ERROR uploading data!!!!!", e); + } + } + + private void doRESTUpload(SharedPreferences prefs, RealmResults<CgmStatusEvent> records) { + String apiScheme = "https://"; + String apiUrl = ""; + String apiSecret = prefs.getString(mContext.getString(R.string.preference_api_secret), "YOURAPISECRET"); + + // TODO - this code needs to go to the Settings Activity. + // Add the extra match for "KEY@" to support the previous single field + Pattern p = Pattern.compile("(.*\\/\\/)?(.*@)?([^\\/]*)(.*)"); + Matcher m = p.matcher(prefs.getString(mContext.getString(R.string.preference_nightscout_url), "")); + + if (m.find()) { + apiUrl = m.group(3); + + // Only override apiSecret from URL (the "old" way), if the API secret preference is empty + if (apiSecret.equals("YOURAPISECRET") || apiSecret.equals("")) { + apiSecret = (m.group(2) == null) ? "" : m.group(2).replace("@", ""); + } + + // Override the URI scheme if it's been provided in the preference) + if (m.group(1) != null && !m.group(1).equals("")) { + apiScheme = m.group(1); + } + } + + // Update the preferences to match what we expect. Only really used from converting from the + // old format to the new format. Aren't we nice for managing backward compatibility? + prefs.edit().putString(mContext.getString(R.string.preference_api_secret), apiSecret).apply(); + prefs.edit().putString(mContext.getString(R.string.preference_nightscout_url), String.format("%s%s", apiScheme, apiUrl)).apply(); + + String uploadUrl = String.format("%s%s@%s/api/v1/", apiScheme, apiSecret, apiUrl); + + try { + doRESTUploadTo(uploadUrl, records); + } catch (Exception e) { + Log.e(TAG, "Unable to do REST API Upload to: " + uploadUrl, e); + } + } + + private void doRESTUploadTo(String baseURI, RealmResults<CgmStatusEvent> records) { + try { + String baseURL; + String secret = null; + String[] uriParts = baseURI.split("@"); + + if (uriParts.length == 1) { + throw new Exception("Starting with API v1, a pass phase is required"); + } else if (uriParts.length == 2) { + secret = uriParts[0]; + baseURL = uriParts[1]; + + // new format URL! + if (secret.contains("http")) { + if (secret.contains("https")) { + baseURL = "https://" + baseURL; + } else { + baseURL = "http://" + baseURL; + } + String[] uriParts2 = secret.split("//"); + secret = uriParts2[1]; + } + } else { + throw new Exception(String.format("Unexpected baseURI: %s, uriParts.length: %s", baseURI, uriParts.length)); + } + + HttpParams params = new BasicHttpParams(); + HttpConnectionParams.setSoTimeout(params, SOCKET_TIMEOUT); + HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT); + + DefaultHttpClient httpclient = new DefaultHttpClient(params); + + postDeviceStatus(baseURL, httpclient); + + for (CgmStatusEvent record : records) { + String postURL = baseURL + "entries"; + + Log.i(TAG, "postURL: " + postURL); + + HttpPost post = new HttpPost(postURL); + + if (secret == null || secret.isEmpty()) { + throw new Exception("Starting with API v1, a pass phase is required"); + } else { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + byte[] bytes = secret.getBytes("UTF-8"); + digest.update(bytes, 0, bytes.length); + bytes = digest.digest(); + StringBuilder sb = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + sb.append(String.format("%02x", b & 0xff)); + } + String token = sb.toString(); + post.setHeader("api-secret", token); + } + + JSONObject json = new JSONObject(); + + try { + // FIXME - Change this to bulk uploads + populateV1APIEntry(json, record); + } catch (Exception e) { + Log.w(TAG, "Unable to populate entry", e); + continue; + } + + String jsonString = json.toString(); + + Log.i(TAG, "Upload JSON: " + jsonString); + + try { + StringEntity se = new StringEntity(jsonString); + post.setEntity(se); + post.setHeader("Accept", "application/json"); + post.setHeader("Content-type", "application/json"); + + ResponseHandler responseHandler = new BasicResponseHandler(); + httpclient.execute(post, responseHandler); + } catch (Exception e) { + Log.w(TAG, "Unable to post data to: '" + post.getURI().toString() + "'", e); + } + + // Yay! We uploaded. Tell Realm + mRealm.beginTransaction(); + // TODO - does realm have auto incrementing keys? + // Turns out not yet (https://github.com/realm/realm-java/issues/469), + // but in the meantime, use this: https://gist.github.com/carloseduardosx/a7bd88d7337660cd10a2c5dcc580ebd0 + RealmResults<CgmStatusEvent> updateRecordResults = mRealm + .where(CgmStatusEvent.class) + .equalTo("eventDate", record.getEventDate()) + .equalTo("deviceName", record.getDeviceName()) + .equalTo("sgv", record.getSgv()) + .findAll(); + // FIXME - We shouldn't need this after we remove insertion of duplicates + for (CgmStatusEvent updateRecord : updateRecordResults) { + updateRecord.setUploaded(true); + } + mRealm.commitTransaction(); + } + } catch (Exception e) { + Log.e(TAG, "Unable to post data", e); + } + } + + private void postDeviceStatus(String baseURL, DefaultHttpClient httpclient) throws Exception { + String devicestatusURL = baseURL + "devicestatus"; + Log.i(TAG, "devicestatusURL: " + devicestatusURL); + + JSONObject json = new JSONObject(); + json.put("uploaderBattery", MainActivity.batLevel); + json.put("device", MainActivity.pumpStatusRecord.getDeviceName()); + + JSONObject pumpInfo = new JSONObject(); + pumpInfo.put("clock", ISO8601_DATE_FORMAT.format(MainActivity.pumpStatusRecord.pumpDate)); + pumpInfo.put("reservoir", MainActivity.pumpStatusRecord.reservoirAmount); + + JSONObject iob = new JSONObject(); + iob.put("timestamp", MainActivity.pumpStatusRecord.pumpDate); + iob.put("bolusiob", MainActivity.pumpStatusRecord.activeInsulin); + + JSONObject battery = new JSONObject(); + battery.put("percent", MainActivity.pumpStatusRecord.batteryPercentage); + + pumpInfo.put("iob", iob); + pumpInfo.put("battery", battery); + json.put("pump", pumpInfo); + String jsonString = json.toString(); + Log.i(TAG, "Device Status JSON: " + jsonString); + + HttpPost post = new HttpPost(devicestatusURL); + StringEntity se = new StringEntity(jsonString); + post.setEntity(se); + post.setHeader("Accept", "application/json"); + post.setHeader("Content-type", "application/json"); + + ResponseHandler responseHandler = new BasicResponseHandler(); + httpclient.execute(post, responseHandler); + } + + private void populateV1APIEntry(JSONObject json, CgmStatusEvent pumpRecord) throws Exception { + // TODO replace with Retrofit/EntriesSerializer + json.put("sgv", pumpRecord.getSgv()); + json.put("direction", EntriesSerializer.getDirectionString(pumpRecord.getTrend())); + json.put("device", pumpRecord.getDeviceName()); + json.put("type", "sgv"); + json.put("date", pumpRecord.getEventDate().getTime()); + json.put("dateString", pumpRecord.getEventDate()); + + } + + private boolean isOnline() { + ConnectivityManager cm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo netInfo = cm.getActiveNetworkInfo(); + return netInfo != null && netInfo.isConnectedOrConnecting(); + } + + public final class Constants { + public static final String ACTION_STATUS_MESSAGE = "info.nightscout.android.upload.nightscout.STATUS_MESSAGE"; + + public static final String EXTENDED_DATA = "info.nightscout.android.upload.nightscout.DATA"; + } +} diff --git a/app/src/main/java/info/nightscout/android/upload/nightscout/serializer/EntriesSerializer.java b/app/src/main/java/info/nightscout/android/upload/nightscout/serializer/EntriesSerializer.java new file mode 100644 index 0000000000000000000000000000000000000000..02da1b07766c5e7f2a3ce413f791c380ca977d7e --- /dev/null +++ b/app/src/main/java/info/nightscout/android/upload/nightscout/serializer/EntriesSerializer.java @@ -0,0 +1,57 @@ +package info.nightscout.android.upload.nightscout.serializer; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import java.lang.reflect.Type; + +import info.nightscout.android.model.CgmStatusEvent; + +/** + * Created by lgoedhart on 26/06/2016. + */ +public class EntriesSerializer implements JsonSerializer<CgmStatusEvent> { + public static String getDirectionString(CgmStatusEvent.TREND trend) { + switch( trend ) { + case NONE: + return "NONE"; + case DOUBLE_UP: + return "DoubleUp"; + case SINGLE_UP: + return "SingleUp"; + case FOURTY_FIVE_UP: + return "FortyFiveUp"; + case FLAT: + return "Flat"; + case FOURTY_FIVE_DOWN: + return "FortyFiveDown"; + case SINGLE_DOWN: + return "SingleDown"; + case DOUBLE_DOWN: + return "DoubleDown"; + case NOT_COMPUTABLE: + return "NOT COMPUTABLE"; + case RATE_OUT_OF_RANGE: + return "RATE OUT OF RANGE"; + case NOT_SET: + return "NONE"; + default: + return "NOT COMPUTABLE"; // TODO - should this be something else? + } + } + + @Override + public JsonElement serialize(CgmStatusEvent src, Type typeOfSrc, JsonSerializationContext context) { + final JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("sgv", src.getSgv()); + jsonObject.addProperty("direction", getDirectionString(src.getTrend())); + jsonObject.addProperty("device", src.getDeviceName()); + jsonObject.addProperty("type", "sgv"); + jsonObject.addProperty("date", src.getEventDate().getTime()); + jsonObject.addProperty("dateString", String.valueOf(src.getEventDate())); + + return jsonObject; + } +} diff --git a/app/src/main/res/drawable/background_splash.xml b/app/src/main/res/drawable/background_splash.xml new file mode 100644 index 0000000000000000000000000000000000000000..161ca05574fa42f9ecff5b430f454f82b5027cd7 --- /dev/null +++ b/app/src/main/res/drawable/background_splash.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + + <item + android:drawable="@color/md_black_1000"/> + + <item> + <bitmap + android:gravity="center" + android:src="@drawable/ic_launcher"/> + </item> + +</layer-list> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index f8dec0427c4c8487c39cce4a993fa624cf926080..56ac4a9c6e3a449922fac8c1bcedbd368be5f4a1 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -2,7 +2,6 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - xmlns:app="http://schemas.android.com/apk/res-auto" android:gravity="center_horizontal" android:orientation="vertical" android:paddingBottom="@dimen/activity_vertical_margin" @@ -47,6 +46,7 @@ android:layout_height="wrap_content" android:hint="@string/prompt_username" android:inputType="text" + android:imeOptions="actionNext" android:maxLines="1" android:singleLine="true"/> @@ -57,7 +57,7 @@ android:hint="@string/prompt_password" android:imeActionId="@+id/login" android:imeActionLabel="@string/action_sign_in_short" - android:imeOptions="actionUnspecified" + android:imeOptions="actionDone|actionUnspecified" android:inputType="textPassword" android:maxLines="1" android:singleLine="true"/> diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 5ecb12545edb96666e30bb41f1e8810666aa2480..b8e7b4b93be8c470d57fcb88bf2d70fa92a9fed9 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -16,7 +16,7 @@ android:layout_height="wrap_content" app:layout_scrollFlags="scroll|enterAlways" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" - app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> + android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> </android.support.v7.widget.Toolbar> @@ -37,7 +37,7 @@ android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginTop="5sp" - android:text="3 minutes ago" + android:text="never" android:textAppearance="?android:attr/textAppearanceMedium" /> <LinearLayout @@ -119,38 +119,13 @@ </LinearLayout> - <LinearLayout - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center_horizontal" - android:orientation="horizontal"> - - <Button - android:id="@+id/button_stop_service" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/button_text_stop_uploading_data" /> - - </LinearLayout> - - <LinearLayout - android:layout_width="wrap_content" + <!-- + <com.github.mikephil.charting.charts.LineChart + android:id="@+id/chart" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_gravity="center_horizontal" - android:orientation="horizontal"> - - <Button - android:id="@+id/button_start_service" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/button_text_get_now" /> - - <Button - android:id="@+id/button_clear_log" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/button_text_clear_log" /> - </LinearLayout> + android:visibility="invisible" /> + --> <ScrollView android:id="@+id/scrollView" diff --git a/app/src/main/res/layout/activity_status.xml b/app/src/main/res/layout/activity_status.xml new file mode 100644 index 0000000000000000000000000000000000000000..eff1081d8b651ab4bebcaae0eab44e4fb08dea67 --- /dev/null +++ b/app/src/main/res/layout/activity_status.xml @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + xmlns:app="http://schemas.android.com/apk/res-auto" + tools:context="info.nightscout.android.medtronic.StatusActivity" + android:orientation="vertical"> + + <android.support.design.widget.AppBarLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> + + <android.support.v7.widget.Toolbar + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:layout_scrollFlags="scroll|enterAlways" + app:popupTheme="@style/ThemeOverlay.AppCompat.Light" + android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> + + </android.support.v7.widget.Toolbar> + + </android.support.design.widget.AppBarLayout> + + <ScrollView + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:id="@+id/status_scroll_view" + android:fillViewport="true" > + + <LinearLayout + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Pump Status" + android:id="@+id/status_pump_text_view" + android:singleLine="true" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Uploader Status" + android:id="@+id/status_uploader_text_view" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="CGM Status" + android:id="@+id/status_cgm_text_view" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Nightscout Status" + android:id="@+id/status_nightscout_text_view" /> + + </LinearLayout> + </ScrollView> + +</LinearLayout> diff --git a/app/src/main/res/menu/menu.xml b/app/src/main/res/menu/menu.xml index ed398f07134bc58c192bc0139912bb2c2ac27c2d..3a4948d2788551963d7d0670e34f0e6c67aa5944 100644 --- a/app/src/main/res/menu/menu.xml +++ b/app/src/main/res/menu/menu.xml @@ -2,13 +2,9 @@ <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <item - android:id="@+id/menu_settings" - android:orderInCategory="100" - android:title="@string/menu_name_preferences"> - </item> - <item - android:id="@+id/registerCNL" - android:orderInCategory="100" - android:title="@string/register_contour_next_link"> - </item> + android:id="@+id/action_menu_status" + android:icon="@drawable/ic_launcher" + android:orderInCategory="100" + app:showAsAction="always" + android:title="@string/menu_name_status"/> </menu> \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml new file mode 100644 index 0000000000000000000000000000000000000000..7ce840eb60032b49e97e6a7569f955b7a4ba9437 --- /dev/null +++ b/app/src/main/res/values/attrs.xml @@ -0,0 +1,12 @@ +<resources> + + <!-- Declare custom theme attributes that allow changing which styles are + used for button bars depending on the API level. + ?android:attr/buttonBarStyle is new as of API 11 so this is + necessary to support previous API levels. --> + <declare-styleable name="ButtonBarContainerTheme"> + <attr name="metaButtonBarStyle" format="reference" /> + <attr name="metaButtonBarButtonStyle" format="reference" /> + </declare-styleable> + +</resources> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000000000000000000000000000000000000..327c0604ce2213706c63cb482ad34c50ec395f25 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ +<resources> + + <color name="black_overlay">#66000000</color> + +</resources> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4622e91ea33e751de165b7f071d94a5aa9efb7ea..bbb585428d3ae24f746f6ff16c7ccdb68440663e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,14 +7,14 @@ <string name="eula_accept">Accept</string> <string name="eula_refuse">Refuse</string> <string-array name="calib_types_values"> - <item>1</item> - <item>2</item> - <item>3</item> + <item>1</item> + <item>2</item> + <item>3</item> </string-array> <string-array name="levelList"> - <item>Error</item> - <item>Info</item> - <item>Debug</item> + <item>Error</item> + <item>Info</item> + <item>Debug</item> </string-array> <string name="title_activity_login">CareLink login</string> @@ -33,6 +33,7 @@ <string name="register_contour_next_link">Register Contour Next Link</string> <string name="preferences_enable_crashlytics">prefs_enable_crashlytics</string> <string name="preferences_enable_answers">prefs_enable_answers</string> + <string name="preferences_enable_remote_logcat">prefs_enable_remote_logcat</string> <string name="menu_name_preferences">Preferences</string> <string name="button_text_stop_uploading_data">Stop Uploading CGM Data</string> <string name="button_text_clear_log">Clear Log</string> @@ -43,4 +44,9 @@ <string name="error_msg_api_secret_length">API Secret must be 12 characters or longer.</string> <string name="text_unit_mmolxl">mmol/L</string> <string name="text_unit_mgxdl">mg/dL</string> + + <string name="title_activity_status">Uploader Status</string> + <string name="dummy_button">Dummy Button</string> + <string name="dummy_content">DUMMY\nCONTENT</string> + <string name="menu_name_status">Status</string> </resources> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index d5d511766e2bf455a639fbe087a72025766bf3e2..98bc8f595e27b154ad08893a68cdabb53fd4db4e 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -7,12 +7,15 @@ <item name="material_drawer_background">@color/material_drawer_dark_background</item> <item name="material_drawer_primary_text">@color/material_drawer_dark_primary_text</item> <item name="material_drawer_primary_icon">@color/material_drawer_dark_primary_icon</item> - <item name="material_drawer_secondary_text">@color/material_drawer_dark_secondary_text</item> + <item name="material_drawer_secondary_text">@color/material_drawer_dark_secondary_text + </item> <item name="material_drawer_hint_text">@color/material_drawer_dark_hint_text</item> <item name="material_drawer_divider">@color/material_drawer_dark_divider</item> <item name="material_drawer_selected">@color/material_drawer_dark_selected</item> <item name="material_drawer_selected_text">@color/material_drawer_dark_selected_text</item> - <item name="material_drawer_header_selection_text">@color/material_drawer_dark_header_selection_text</item> + <item name="material_drawer_header_selection_text"> + @color/material_drawer_dark_header_selection_text + </item> </style> <!-- Settings theme. Has an action bar. --> @@ -20,4 +23,20 @@ </style> + <style name="SplashTheme" parent="Theme.AppCompat.NoActionBar"> + <item name="android:windowBackground">@drawable/background_splash</item> + </style> + + <style name="FullscreenTheme" parent="AppTheme"> + <item name="android:actionBarStyle">@style/FullscreenActionBarStyle</item> + <item name="android:windowActionBarOverlay">true</item> + <item name="android:windowBackground">@null</item> + <item name="metaButtonBarStyle">?android:attr/buttonBarStyle</item> + <item name="metaButtonBarButtonStyle">?android:attr/buttonBarButtonStyle</item> + </style> + + <style name="FullscreenActionBarStyle" parent="Widget.AppCompat.ActionBar"> + <item name="android:background">@color/black_overlay</item> + </style> + </resources> diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 3a039ba36f60428eed118a73ba7734eac5e66c59..8a8fb08d3852284649d8428d84e72fac42c4d557 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -66,6 +66,11 @@ android:key="@string/preferences_enable_answers" android:summary="Sends usage data to the developer to help develop a better app." android:title="Share usage data" /> + <SwitchPreference + android:defaultValue="false" + android:key="@string/preferences_enable_remote_logcat" + android:summary="Allow the developer to debug your app. Only enable if asked to by the support team." + android:title="Remote Debugging" /> <ListPreference android:defaultValue="1" android:disableDependentsState="false"