From 2c47239f6e706f60c53526fcdc87cf2f02134cfb Mon Sep 17 00:00:00 2001 From: Nigreon Date: Wed, 8 Jul 2020 23:56:29 +0200 Subject: [PATCH] First Commit --- app/.gitignore | 1 + app/build.gradle | 34 + app/libs/FastBLE-2.3.4.jar | Bin 0 -> 77789 bytes app/proguard-rules.pro | 21 + .../nigreon/blegps/ExampleInstrumentedTest.kt | 24 + app/src/main/AndroidManifest.xml | 35 + .../java/net/nigreon/blegps/BLEHRService.kt | 224 +++++++ .../nigreon/blegps/BLEMockLocationService.kt | 378 +++++++++++ .../java/net/nigreon/blegps/BLEProvider.kt | 389 +++++++++++ .../net/nigreon/blegps/GPSConfiguration.kt | 16 + .../java/net/nigreon/blegps/MainActivity.kt | 614 ++++++++++++++++++ .../nigreon/blegps/MockLocationProvider.kt | 40 ++ .../net/nigreon/blegps/ServicesConstants.kt | 73 +++ .../java/net/nigreon/blegps/TaskerReceiver.kt | 23 + .../drawable-v24/ic_launcher_foreground.xml | 34 + .../res/drawable/ic_launcher_background.xml | 170 +++++ app/src/main/res/layout/activity_main.xml | 338 ++++++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2963 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4905 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2060 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2783 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4490 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 6895 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6387 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10413 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9128 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15132 bytes app/src/main/res/values/colors.xml | 6 + app/src/main/res/values/strings.xml | 76 +++ app/src/main/res/values/styles.xml | 11 + .../net/nigreon/blegps/ExampleUnitTest.kt | 17 + build.gradle | 28 + gradle.properties | 15 + settings.gradle | 1 + 36 files changed, 2578 insertions(+) create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/libs/FastBLE-2.3.4.jar create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/net/nigreon/blegps/ExampleInstrumentedTest.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/net/nigreon/blegps/BLEHRService.kt create mode 100644 app/src/main/java/net/nigreon/blegps/BLEMockLocationService.kt create mode 100644 app/src/main/java/net/nigreon/blegps/BLEProvider.kt create mode 100644 app/src/main/java/net/nigreon/blegps/GPSConfiguration.kt create mode 100644 app/src/main/java/net/nigreon/blegps/MainActivity.kt create mode 100644 app/src/main/java/net/nigreon/blegps/MockLocationProvider.kt create mode 100644 app/src/main/java/net/nigreon/blegps/ServicesConstants.kt create mode 100644 app/src/main/java/net/nigreon/blegps/TaskerReceiver.kt create mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/test/java/net/nigreon/blegps/ExampleUnitTest.kt create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 settings.gradle diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..015cb99 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,34 @@ +apply plugin: 'com.android.application' + +apply plugin: 'kotlin-android' + +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 28 + buildToolsVersion "29.0.1" + defaultConfig { + applicationId "net.nigreon.blegps" + minSdkVersion 25 + targetSdkVersion 25 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'com.android.support:appcompat-v7:28.0.0' + implementation 'com.android.support.constraint:constraint-layout:1.1.3' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' +} diff --git a/app/libs/FastBLE-2.3.4.jar b/app/libs/FastBLE-2.3.4.jar new file mode 100644 index 0000000000000000000000000000000000000000..a2a8e5dfff9dabc394b619a42597401121147f2e GIT binary patch literal 77789 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 Vr_;A@&#R-KGi9DvGmoz9-fz>-oXY?J literal 0 HcmV?d00001 diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/androidTest/java/net/nigreon/blegps/ExampleInstrumentedTest.kt b/app/src/androidTest/java/net/nigreon/blegps/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..bdce342 --- /dev/null +++ b/app/src/androidTest/java/net/nigreon/blegps/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package net.nigreon.blegps + +import android.support.test.InstrumentationRegistry +import android.support.test.runner.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getTargetContext() + assertEquals("net.nigreon.blegps", appContext.packageName) + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..649ccde --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/net/nigreon/blegps/BLEHRService.kt b/app/src/main/java/net/nigreon/blegps/BLEHRService.kt new file mode 100644 index 0000000..e5c5656 --- /dev/null +++ b/app/src/main/java/net/nigreon/blegps/BLEHRService.kt @@ -0,0 +1,224 @@ +package net.nigreon.blegps + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.bluetooth.BluetoothGatt +import android.content.Intent +import android.os.Binder +import android.os.Build +import android.os.IBinder +import android.support.v4.app.NotificationCompat +import android.support.v4.content.LocalBroadcastManager +import android.util.Log +import com.clj.fastble.BleManager +import com.clj.fastble.callback.BleGattCallback +import com.clj.fastble.callback.BleNotifyCallback +import com.clj.fastble.callback.BleWriteCallback +import com.clj.fastble.data.BleDevice +import com.clj.fastble.exception.BleException +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.util.* + +class BLEHRService : Service() { + private val LOG_TAG = "BLEHRService" + private val CHANNEL_ID = "NotifID2" + private val binder = sBinder() + + val timerActivity = Timer() + + private lateinit var bleDevice: BleDevice + private val serviceHeartRateUuid: String = "0000180d-0000-1000-8000-00805f9b34fb" + private val characteristicHRControlPointUuid: String = "00002a39-0000-1000-8000-00805f9b34fb" + private val characteristicHRMeasurementUuid: String = "00002a37-0000-1000-8000-00805f9b34fb" + + private fun initNotificationHR() { + BleManager.getInstance().notify( + bleDevice, + serviceHeartRateUuid, + characteristicHRMeasurementUuid, + object : BleNotifyCallback() { + override fun onNotifySuccess() { + Log.v(LOG_TAG, "Notify HR Success") + //initNotificationPressure() + } + + override fun onNotifyFailure(exception: BleException) { + Log.v(LOG_TAG, "BLE Notify HR Failure $exception") + //initNotificationPressure() + } + + override fun onCharacteristicChanged(data: ByteArray) { + //Log.v(LOG_TAG, "Notify Temperature Characteristic changed") + val buffer = ByteBuffer.wrap(data) + buffer.order(ByteOrder.LITTLE_ENDIAN) + //texttemp.text = buffer.float.toString() + LocalBroadcastManager.getInstance(baseContext).sendBroadcast( + Intent(ServicesConstants.BROADCAST_FILTER.FILTERBTHRM).putExtra(ServicesConstants.BROADCAST_KEY.KEYBTHRM, buffer.get(1)) + ) + } + }) + } + + private fun startHRBLE() { + BleManager.getInstance().connect("CC:2C:24:71:9C:BC", object : BleGattCallback() { + override fun onStartConnect() { + + } + + override fun onConnectFail(bleDevice: BleDevice, exception: BleException) { + Log.v(LOG_TAG, "BLE HR Connection Failed $exception") + } + + override fun onConnectSuccess( + bleDeviceConnect: BleDevice, + gattConnect: BluetoothGatt, + status: Int + ) { + Log.v(LOG_TAG, "BLE HR Connection OK") + LocalBroadcastManager.getInstance(baseContext).sendBroadcast( + Intent(ServicesConstants.BROADCAST_FILTER.FILTERBTHR).putExtra( + ServicesConstants.BROADCAST_KEY.KEYBTSTATUSHR, + true + ) + ) + bleDevice = bleDeviceConnect + + initNotificationHR() + + val t = object : TimerTask() { + override fun run() { + startHRBLEActivity() + } + } + timerActivity.scheduleAtFixedRate(t, 0, 9500) + + } + + override fun onDisConnected( + isActiveDisConnected: Boolean, + bleDevice: BleDevice, + gatt: BluetoothGatt, + status: Int + ) { + LocalBroadcastManager.getInstance(baseContext).sendBroadcast( + Intent(ServicesConstants.BROADCAST_FILTER.FILTERBTHR).putExtra( + ServicesConstants.BROADCAST_KEY.KEYBTSTATUSHR, + false + ) + ) + timerActivity.cancel() + Log.v(LOG_TAG, "BLE HR Disconnected") + } + }) + } + + private fun stopHRBLE() { + LocalBroadcastManager.getInstance(baseContext).sendBroadcast( + Intent(ServicesConstants.BROADCAST_FILTER.FILTERBTHR).putExtra(ServicesConstants.BROADCAST_KEY.KEYBTSTATUSHR, false) + ) + + BleManager.getInstance().disconnect(bleDevice) + } + + private fun startHRBLEActivity() { + val data: ByteArray = byteArrayOf(0x15,0x01,0x01) + BleManager.getInstance().write( + bleDevice, + serviceHeartRateUuid, + characteristicHRControlPointUuid, + data, + object : BleWriteCallback() { + override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray) { + //Log.v(LOG_TAG, "Start Activity sended") + } + + override fun onWriteFailure(exception: BleException) { + Log.v(LOG_TAG, "Start Activity Failure $exception") + } + }) + } + + private fun stopHRBLEActivity() { + val data: ByteArray = byteArrayOf(0x15,0x01,0x00) + BleManager.getInstance().write( + bleDevice, + serviceHeartRateUuid, + characteristicHRControlPointUuid, + data, + object : BleWriteCallback() { + override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray) { + Log.v(LOG_TAG, "Stop Activity sended") + } + + override fun onWriteFailure(exception: BleException) { + Log.v(LOG_TAG, "Stop Activity send Failure $exception") + } + }) + } + + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + if(intent.action == ServicesConstants.HRSERVICEACTION.STARTFOREGROUND_ACTION) { + Log.i(LOG_TAG, "Received Start Foreground Intent ") + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // Create the NotificationChannel + val name = "ChannelName" + val descriptionText = "ChannelDescription" + val importance = NotificationManager.IMPORTANCE_DEFAULT + val mChannel = NotificationChannel(CHANNEL_ID, name, importance) + mChannel.description = descriptionText + // Register the channel with the system; you can't change the importance + // or other notification behaviors after this + val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(mChannel) + } + + var notification = NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_launcher_foreground) + .setContentTitle("BLE HR") + .setContentText("BLE HR active") + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + //.setContentIntent(pendingIntent) + .build() + + //pBLE = BLEProvider(application, baseContext) + startHRBLE() + + startForeground(ServicesConstants.NOTIFICATION_ID.FOREGROUND_HR_SERVICE,notification) + } else if(intent.action == ServicesConstants.HRSERVICEACTION.STOPFOREGROUND_ACTION) { + Log.i(LOG_TAG, "Received Stop Foreground Intent ") + //pBLE.disconnectBLE() + stopHRBLE() + stopForeground(true) + stopSelf() + } + return START_STICKY + } + + override fun onDestroy() { + super.onDestroy() + Log.i(LOG_TAG, "In onDestroy") + } + + override fun onBind(intent: Intent): IBinder? { + Log.v(LOG_TAG, "in onBind") + return binder + } + + override fun onRebind(intent: Intent) { + Log.v(LOG_TAG, "in onRebind") + super.onRebind(intent) + } + + override fun onUnbind(intent: Intent): Boolean { + Log.v(LOG_TAG, "in onUnbind") + return true + } + + inner class sBinder : Binder() { + // Return this instance of LocalService so clients can call public methods + fun getService(): BLEHRService = this@BLEHRService + } +} \ No newline at end of file diff --git a/app/src/main/java/net/nigreon/blegps/BLEMockLocationService.kt b/app/src/main/java/net/nigreon/blegps/BLEMockLocationService.kt new file mode 100644 index 0000000..bfb49bd --- /dev/null +++ b/app/src/main/java/net/nigreon/blegps/BLEMockLocationService.kt @@ -0,0 +1,378 @@ +package net.nigreon.blegps + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.location.LocationManager +import android.os.* +import android.support.v4.app.NotificationCompat +import android.support.v4.content.LocalBroadcastManager +import android.util.Log +import java.io.File +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.text.SimpleDateFormat +import java.util.* +import kotlin.experimental.or + + +class BLEMockLocationService : Service() { + private val LOG_TAG = "BLEMockLocationService" + private val CHANNEL_ID = "NotifID" + public val ACTIVATION_CHANGE = 1 + private lateinit var pBLE: BLEProvider + private lateinit var mockGPS: MockLocationProvider + private var locationReceived = false + private var qualityReceived = false + private var gpsverbose = false + private var gpxopened = false + private var hrenable = false + private var tempenable = false + private lateinit var gpxoutputstream: FileOutputStream + + var gpsConf: GPSConfiguration = GPSConfiguration() + + // Location + private var currentLatitude: Double = -1.0 + private var currentLongitude: Double = -1.0 + private var currentElevation: Double = -1.0 + private var currentSpeed: Float = -1.0f + private var currentHeading: Float = -1.0f + private var currentEHPE: Float = -1.0f + // Quality + private var currentEVPE: Float = -1.0f + private var currentHDOP: Float = -1.0f + private var currentVDOP: Float = -1.0f + private var currentSatview: Byte = -1 + private var currentSatused: Byte = -1 + private var currentElevationGPS: Double = -1.0 + + private var currentHR: Byte = 0 + private var currentTemperature: Float = 0.0f + + private val binder = sBinder() + + fun locationChanged(intent: Intent) + { + Log.v(LOG_TAG, "Latitude Service Broadcast received") + currentLatitude = intent.getDoubleExtra(ServicesConstants.BROADCAST_KEY.KEYLOCATIONLAT,-1.0) + currentLongitude = intent.getDoubleExtra(ServicesConstants.BROADCAST_KEY.KEYLOCATIONLON,-1.0) + currentEHPE = intent.getFloatExtra(ServicesConstants.BROADCAST_KEY.KEYLOCATIONEHPE,-1.0f) + currentElevation = intent.getDoubleExtra(ServicesConstants.BROADCAST_KEY.KEYLOCATIONELE,-1.0) + currentSpeed = intent.getFloatExtra(ServicesConstants.BROADCAST_KEY.KEYLOCATIONSPEED,-1.0f) + currentHeading = intent.getFloatExtra(ServicesConstants.BROADCAST_KEY.KEYLOCATIONHEADING,-1.0f) + mockGPS.pushLocation(currentLatitude,currentLongitude,currentElevation, currentSpeed, currentHeading, currentEHPE) + locationReceived = true + storeGPX() + } + + fun qualityChanged(intent: Intent) + { + //Log.v(LOG_TAG, "Latitude Service Broadcast received") + currentEHPE = intent.getFloatExtra(ServicesConstants.BROADCAST_KEY.KEYQUALITYEHPE,-1.0f) + currentEVPE = intent.getFloatExtra(ServicesConstants.BROADCAST_KEY.KEYQUALITYEVPE,-1.0f) + currentHDOP = intent.getFloatExtra(ServicesConstants.BROADCAST_KEY.KEYQUALITYHDOP,-1.0f) + currentVDOP = intent.getFloatExtra(ServicesConstants.BROADCAST_KEY.KEYQUALITYVDOP,-1.0f) + currentSatview = intent.getByteExtra(ServicesConstants.BROADCAST_KEY.KEYQUALITYSATVIEW,-1) + currentSatused = intent.getByteExtra(ServicesConstants.BROADCAST_KEY.KEYQUALITYSATUSED,-1) + currentElevationGPS = intent.getDoubleExtra(ServicesConstants.BROADCAST_KEY.KEYQUALITYELEGPS,-1.0) + qualityReceived = true + storeGPX() + } + + fun HRChanged(intent: Intent) + { + currentHR = intent.getByteExtra(ServicesConstants.BROADCAST_KEY.KEYBTHRM,-1) + } + + fun bmpStatusChanged(intent: Intent) + { + tempenable = intent.getBooleanExtra(ServicesConstants.BROADCAST_KEY.KEYBMPSTATUS, false) + } + + fun tempChanged(intent: Intent) + { + currentTemperature = intent.getFloatExtra(ServicesConstants.BROADCAST_KEY.KEYTEMP, -1.0f) + } + + fun taskerChanged(bundle: Bundle) { + Log.v(LOG_TAG, "taskerChanged") + if(bundle.containsKey(ServicesConstants.BROADCAST_KEY.KEYBOOST)) { + /*if (bundle.getBoolean(ServicesConstants.BROADCAST_KEY.KEYBOOST)) { + //gpsConf.gpsBoost = intent.getBooleanExtra(ServicesConstants.BROADCAST_KEY.KEYBOOST, false) + gpsConf.gpsBoost = true + } else { + gpsConf.gpsBoost = false + }*/ + gpsConf.gpsBoost = bundle.getBoolean(ServicesConstants.BROADCAST_KEY.KEYBOOST) + sendConfBLE(gpsConf) + Log.v(LOG_TAG, "Received Tasker Intent : Boost ${gpsConf.gpsBoost}") + } + } + + fun storeGPX() + { + if(gpxopened) { + if(!gpsverbose and locationReceived) + { + //writeGPX("${SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(Date())} $currentLatitude $currentLongitude $currentElevation m\n") + writeGPX("$currentElevation\n") + locationReceived = false + qualityReceived = false + } else if(gpsverbose and locationReceived and qualityReceived) { + //writeGPX("${SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(Date())} $currentLatitude $currentLongitude $currentElevation m\n") + //writeGPX("$currentEHPE m $currentEVPE m $currentHDOP $currentVDOP $currentSatused/$currentSatview $currentElevationGPS m\n") + //if(hrenable) { writeGPX("$currentHR bpm\n")} + //if(tempenable) { writeGPX("${currentTemperature}°C\n")} + writeGPX("$currentElevation\n" + + "\n") + if(hrenable or tempenable) + { + writeGPX("") + if(hrenable) + { + writeGPX("$currentHR") + } + if(tempenable) + { + writeGPX("$currentTemperature") + } + writeGPX("\n") + } + + writeGPX("$currentEHPE$currentEVPE$currentHDOP$currentVDOP$currentSatused$currentSatview$currentElevationGPS\n") + writeGPX("\n\n") + locationReceived = false + qualityReceived = false + } + } + } + + fun writeGPX(str: String) + { + try { + gpxoutputstream.write(str.toByteArray()) + } catch (e: FileNotFoundException) { + e.printStackTrace() + } catch (e: IOException) { + e.printStackTrace() + } + + } + + override fun onCreate() { + super.onCreate() + + mockGPS = MockLocationProvider(LocationManager.GPS_PROVIDER, baseContext) + + val brReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + when (intent?.action) { + ServicesConstants.BROADCAST_FILTER.FILTERLOCATION -> locationChanged(intent) + ServicesConstants.BROADCAST_FILTER.FILTERQUALITY-> qualityChanged(intent) + ServicesConstants.BROADCAST_FILTER.FILTERBTHRM-> HRChanged(intent) + ServicesConstants.BROADCAST_FILTER.FILTERTEMP-> tempChanged(intent) + ServicesConstants.BROADCAST_FILTER.FILTERBMP-> bmpStatusChanged(intent) + + + //BROADCAST_CHANGE_TYPE_CHANGED -> handleChangeTypeChanged() + } + } + } + + val manager = LocalBroadcastManager.getInstance(this) + manager.registerReceiver(brReceiver, IntentFilter(ServicesConstants.BROADCAST_FILTER.FILTERLOCATION)) + manager.registerReceiver(brReceiver, IntentFilter(ServicesConstants.BROADCAST_FILTER.FILTERQUALITY)) + manager.registerReceiver(brReceiver, IntentFilter(ServicesConstants.BROADCAST_FILTER.FILTERBTHRM)) + manager.registerReceiver(brReceiver, IntentFilter(ServicesConstants.BROADCAST_FILTER.FILTERTEMP)) + manager.registerReceiver(brReceiver, IntentFilter(ServicesConstants.BROADCAST_FILTER.FILTERBMP)) + //pBLE.registerBMP() + } + + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + if(intent.action == ServicesConstants.MOCKSERVICEACTION.STARTFOREGROUND_ACTION) { + Log.i(LOG_TAG, "Received Start Foreground Intent ") + + // val intentNotif = Intent(this, MainActivity::class.java) +// intentNotif.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + // intentNotif.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACT + + //val pendingIntent = PendingIntent.getActivity(this, 0, intentNotif, 0) + + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // Create the NotificationChannel + val name = "ChannelName" + val descriptionText = "ChannelDescription" + val importance = NotificationManager.IMPORTANCE_DEFAULT + val mChannel = NotificationChannel(CHANNEL_ID, name, importance) + mChannel.description = descriptionText + // Register the channel with the system; you can't change the importance + // or other notification behaviors after this + val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(mChannel) + } + + var notification = NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_launcher_foreground) + .setContentTitle("BLE GPS") + .setContentText("BLE & Mock active") + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + //.setContentIntent(pendingIntent) + .build() + + pBLE = BLEProvider(application, baseContext) + mockGPS.start() + startForeground(ServicesConstants.NOTIFICATION_ID.FOREGROUND_MOCK_SERVICE,notification) + } else if(intent.action == ServicesConstants.MOCKSERVICEACTION.STOPFOREGROUND_ACTION) { + Log.i(LOG_TAG, "Received Stop Foreground Intent ") + pBLE.disconnectBLE() + mockGPS.shutdown() + stopForeground(true) + stopSelf() + } else if(intent.action == ServicesConstants.MOCKSERVICEACTION.TASKER_ACTION){ + Log.i(LOG_TAG, "intent action : ${intent.action}") + taskerChanged(intent.extras) + } + return START_STICKY + } + override fun onDestroy() { + super.onDestroy() + Log.i(LOG_TAG, "In onDestroy") + } + + override fun onBind(intent: Intent): IBinder? { + Log.v(LOG_TAG, "in onBind") + return binder + } + + override fun onRebind(intent: Intent) { + Log.v(LOG_TAG, "in onRebind") + super.onRebind(intent) + } + + override fun onUnbind(intent: Intent): Boolean { + Log.v(LOG_TAG, "in onUnbind") + return true + } + + fun sendCalibrateBLE(calibrateval: Int) + { + val buffer = ByteBuffer.allocate(4) + buffer.order(ByteOrder.LITTLE_ENDIAN) + buffer.putInt(calibrateval) + pBLE.sendCalibrate(buffer.array()) + } + + fun HRStatusChanged(enable: Boolean) + { + hrenable = enable + } + + fun openGPX(datefilename: String) { + //val datefilename: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date()) + var gpxfilename = File( + Environment.getExternalStorageDirectory().path + File.separator + "BTGPS", + datefilename + ".gpx" + ) + var gpxdir = File(Environment.getExternalStorageDirectory().path + File.separator + "BTGPS") + var success = true; + if (!gpxdir.exists()) { + success = gpxdir.mkdir(); + } + + if (success) { + if(!gpxfilename.exists()) { + try { + gpxoutputstream = FileOutputStream(gpxfilename, true) + //val gpxheader = "GPX Header\n" + val gpxheader = + "\n" + + "\n" + + "" + + "${datefilename}" + + "${datefilename}\n" + + "\n" + + "\n" + + "\n" + + "${datefilename}\n" + + "\n" + writeGPX(gpxheader) + } catch (e: FileNotFoundException) { + e.printStackTrace() + } catch (e: IOException) { + e.printStackTrace() + } + } else { + try { + gpxoutputstream = FileOutputStream(gpxfilename, true) + } catch (e: FileNotFoundException) { + e.printStackTrace() + } catch (e: IOException) { + e.printStackTrace() + } + } + gpxopened = true + } + } + + fun closeGPX() + { + //val gpxfooter = "GPX Footer\n" + val gpxfooter = "\n" + writeGPX(gpxfooter) + gpxoutputstream.close() + gpxopened = false + } + + //fun sendConfBLE(bmpnrf: Boolean, bmpble: Boolean, bmp2gps: Boolean, exttemp: Boolean, gpsble: Boolean, gpsbleverb: Boolean, gpsserial: Boolean, gpsmode: Int, gpspoller: Short, gpssendbt: Short, bmppoller: Short) { + fun sendConfBLE(gpsConfNew: GPSConfiguration) { + gpsConf = gpsConfNew + val buffer = ByteBuffer.allocate(8) + buffer.order(ByteOrder.LITTLE_ENDIAN) + var activateByte1: Byte = 0 + var activateByte2: Byte = 0 + + if(gpsConf.bmpNrf) { activateByte1 = activateByte1.or(0x01) } + if(gpsConf.bmpBT) { activateByte1 = activateByte1.or(0x02) } + if(gpsConf.bmp2gps) { activateByte1 = activateByte1.or(0x04) } + if(gpsConf.extTemp) { activateByte1 = activateByte1.or(0x08) } + + if(gpsConf.gpsBLE) { activateByte2 = activateByte2.or(0x01) } + if(gpsConf.gpsVerbose) { activateByte2 = activateByte2.or(0x02) } + if(gpsConf.gpsSerial) { activateByte2 = activateByte2.or(0x04) } + if(gpsConf.gpsMode == 1 || gpsConf.gpsBoost) { + activateByte2 = activateByte2.or(0x08) + } else if(gpsConf.gpsMode == 2) { + activateByte2 = activateByte2.or(0x10) + } + + buffer.put(activateByte1) + buffer.put(activateByte2) + if(gpsConf.gpsBoost) { + buffer.putShort(4) + buffer.putShort(gpsConf.bmpPollerSend) + buffer.putShort(4) + } else { + buffer.putShort(gpsConf.gpsPollerSend) + buffer.putShort(gpsConf.bmpPollerSend) + buffer.putShort(gpsConf.gpsSendBTSend) + } + pBLE.sendConf(buffer.array()) + } + + inner class sBinder : Binder() { + // Return this instance of LocalService so clients can call public methods + fun getService(): BLEMockLocationService = this@BLEMockLocationService + } +} \ No newline at end of file diff --git a/app/src/main/java/net/nigreon/blegps/BLEProvider.kt b/app/src/main/java/net/nigreon/blegps/BLEProvider.kt new file mode 100644 index 0000000..835aa97 --- /dev/null +++ b/app/src/main/java/net/nigreon/blegps/BLEProvider.kt @@ -0,0 +1,389 @@ +package net.nigreon.blegps +import android.app.Application +import android.bluetooth.BluetoothGatt +import android.content.Context +import android.content.Intent +import android.support.v4.content.LocalBroadcastManager +import android.util.Log +import com.clj.fastble.BleManager +import com.clj.fastble.data.BleDevice +import com.clj.fastble.exception.BleException +import com.clj.fastble.callback.BleGattCallback +import com.clj.fastble.callback.BleNotifyCallback +import com.clj.fastble.callback.BleReadCallback +import com.clj.fastble.callback.BleWriteCallback +import java.nio.ByteBuffer +import java.nio.ByteOrder +import android.os.CountDownTimer +import android.os.Handler + + +class BLEProvider(val application: Application, val baseContext: Context) +{ + private lateinit var bleDevice: BleDevice + + private val serviceEnvironmentalSensingUuid: String = "0000181a-0000-1000-8000-00805f9b34fb" + private val serviceLNUuid: String = "00001819-0000-1000-8000-00805f9b34fb" + private val characteristicPressureUuid: String = "00002a6d-0000-1000-8000-00805f9b34fb" + private val characteristicTemperatureUuid: String = "00002a6e-0000-1000-8000-00805f9b34fb" + private val characteristicLSUuid: String = "00002a67-0000-1000-8000-00805f9b34fb" + private val characteristicPQUuid: String = "00002a69-0000-1000-8000-00805f9b34fb" + private val serviceWriteUuid: String = "00002000-0000-1000-8000-00805f9b34fb" + private val characteristicSatUuid: String = "00002002-0000-1000-8000-00805f9b34fb" + private val characteristicWriteUuid: String = "00002001-0000-1000-8000-00805f9b34fb" + private val characteristicCalibrateUuid: String = "00002003-0000-1000-8000-00805f9b34fb" + + private var connected = false + private var reconnectcount = 0 + + + private val LOG_TAG = "BLEProvider" + //private lateinit var baseContext: Context + + + init + { + connectBLE() + } + + fun delayFunction(function: ()-> Unit, delay: Long) { + Handler().postDelayed(function, delay) + } + + private fun connectBLE() + { + //BleManager.getInstance().init(application) + /*BleManager.getInstance() + .enableLog(true) + .setReConnectCount(10, 60000)*/ + + //BleManager.getInstance().operateTimeout = 5000 + //BleManager.getInstance().connect("24:6F:28:16:C1:F2", object : BleGattCallback() { + BleManager.getInstance().connect("E9:8E:8A:12:6F:3F", object : BleGattCallback() { + override fun onStartConnect() { + + } + + override fun onConnectFail(bleDevice: BleDevice, exception: BleException) { + Log.v(LOG_TAG, "BLE Connection Failed $exception") + if(reconnectcount <= 15) { + reconnectcount++ + delayFunction( { connectBLE() }, 60000) + } else { + reconnectcount=0 + LocalBroadcastManager.getInstance(baseContext).sendBroadcast( + Intent(ServicesConstants.BROADCAST_FILTER.FILTERBT).putExtra(ServicesConstants.BROADCAST_KEY.KEYBTSTATUS, false) + ) + connected=false + } + + } + + override fun onConnectSuccess(bleDeviceConnect: BleDevice, gattConnect: BluetoothGatt, status: Int) { + Log.v(LOG_TAG, "BLE Connection OK") + connected = true + reconnectcount = 0 + bleDevice = bleDeviceConnect + //initNotificationTemperature() + object : CountDownTimer(3000, 500) { + var tickCount = 0 + override fun onFinish() { + // When timer is finished + // Execute your code here + LocalBroadcastManager.getInstance(baseContext).sendBroadcast( + Intent(ServicesConstants.BROADCAST_FILTER.FILTERBT).putExtra(ServicesConstants.BROADCAST_KEY.KEYBTSTATUS, true) + ) + } + + override fun onTick(millisUntilFinished: Long) { + //Log.v(LOG_TAG, "Tick: $millisUntilFinished") + when (tickCount) { + 0 -> initNotificationTemperature() + 1 -> initNotificationPQ() + 2 -> initNotificationPressure() + 3 -> initNotificationSat() + 4 -> initNotificationLS() + } + tickCount++ + /*if(millisUntilFinished == 5000L) + { + initNotificationTemperature() + } else if(millisUntilFinished == 4000L) { + initNotificationPQ() + } else if(millisUntilFinished == 3000L) { + initNotificationPressure() + } else if(millisUntilFinished == 2000L) { + initNotificationSat() + } else if(millisUntilFinished == 1000L) { + initNotificationLS() + }*/ + // millisUntilFinished The amount of time until finished. + } + }.start() + //initNotificationPressure() + //initNotificationLS() + + + } + + override fun onDisConnected( + isActiveDisConnected: Boolean, + bleDevice: BleDevice, + gatt: BluetoothGatt, + status: Int + ) { + //connected = false + //LocalBroadcastManager.getInstance(baseContext).sendBroadcast( + // Intent(ServicesConstants.BROADCAST_FILTER.FILTERBT).putExtra(ServicesConstants.BROADCAST_KEY.KEYBTSTATUS, false) + //) + Log.v(LOG_TAG, "BLE Disconnected") + if(connected) { + connectBLE() + } + } + }) + + } + + private fun initNotificationPressure() { + BleManager.getInstance().notify( + bleDevice, + serviceEnvironmentalSensingUuid, + characteristicPressureUuid, + object : BleNotifyCallback() { + override fun onNotifySuccess() { + Log.v(LOG_TAG, "BLE Notify Pressure Success") + //initNotificationLS() + } + + override fun onNotifyFailure(exception: BleException) { + Log.v(LOG_TAG, "BLE Notify Pressure Failure $exception") + //initNotificationLS() + } + + override fun onCharacteristicChanged(data: ByteArray) { + val buffer = ByteBuffer.wrap(data) + buffer.order(ByteOrder.LITTLE_ENDIAN) + Log.v(LOG_TAG, "BLE Notify Pressure Characteristic changed") + //textpressure.text = buffer.getInt().toString() + LocalBroadcastManager.getInstance(baseContext).sendBroadcast( + Intent(ServicesConstants.BROADCAST_FILTER.FILTERPRESSURE).putExtra(ServicesConstants.BROADCAST_KEY.KEYPRESSURE, buffer.int) + ) + } + }) + } + + private fun initNotificationTemperature() { + BleManager.getInstance().notify( + bleDevice, + serviceEnvironmentalSensingUuid, + characteristicTemperatureUuid, + object : BleNotifyCallback() { + override fun onNotifySuccess() { + Log.v(LOG_TAG, "Notify Temperature Success") + //initNotificationPressure() + } + + override fun onNotifyFailure(exception: BleException) { + Log.v(LOG_TAG, "BLE Notify Temperature Failure $exception") + //initNotificationPressure() + } + + override fun onCharacteristicChanged(data: ByteArray) { + Log.v(LOG_TAG, "Notify Temperature Characteristic changed") + val buffer = ByteBuffer.wrap(data) + buffer.order(ByteOrder.LITTLE_ENDIAN) + //texttemp.text = buffer.float.toString() + LocalBroadcastManager.getInstance(baseContext).sendBroadcast( + Intent(ServicesConstants.BROADCAST_FILTER.FILTERTEMP).putExtra(ServicesConstants.BROADCAST_KEY.KEYTEMP, buffer.short/100f) + ) + } + }) + } + + private fun initNotificationLS() { + BleManager.getInstance().notify( + bleDevice, + serviceLNUuid, + characteristicLSUuid, + object : BleNotifyCallback() { + override fun onNotifySuccess() { + Log.v(LOG_TAG, "Notify LS Success") + //initNotificationPQ() + } + + override fun onNotifyFailure(exception: BleException) { + Log.v(LOG_TAG, "BLE Notify LS Failure $exception") + //initNotificationPQ() + } + + @UseExperimental(ExperimentalUnsignedTypes::class) + override fun onCharacteristicChanged(data: ByteArray) { + Log.v(LOG_TAG, "Notify LS Characteristic changed ${data.size}") + val buffer = ByteBuffer.wrap(data) + buffer.order(ByteOrder.LITTLE_ENDIAN) + //val flags: UShort = buffer.short.toUShort() + val speed: Float = buffer.short.toUShort().toFloat()/100.0f + val latitude: Double = buffer.int/10000000.0 + val longitude: Double = buffer.int/10000000.0 + val ehpe: Float = buffer.int.toUInt().toFloat()/100.0f + //val ehpe: Float = 5.0f + val elevation: Double = buffer.int/100.0 + val heading: Float = buffer.short.toUShort().toFloat()/100.0f + Log.v(LOG_TAG, "LS $latitude $longitude $speed $heading") + LocalBroadcastManager.getInstance(baseContext).sendBroadcast( + Intent(ServicesConstants.BROADCAST_FILTER.FILTERLOCATION).putExtra(ServicesConstants.BROADCAST_KEY.KEYLOCATIONLAT, latitude) + .putExtra(ServicesConstants.BROADCAST_KEY.KEYLOCATIONLON, longitude) + .putExtra(ServicesConstants.BROADCAST_KEY.KEYLOCATIONEHPE, ehpe) + .putExtra(ServicesConstants.BROADCAST_KEY.KEYLOCATIONELE, elevation) + .putExtra(ServicesConstants.BROADCAST_KEY.KEYLOCATIONSPEED, speed) + .putExtra(ServicesConstants.BROADCAST_KEY.KEYLOCATIONHEADING, heading) + + ) + } + }) + } + + private fun initNotificationPQ() { + BleManager.getInstance().notify( + bleDevice, + serviceLNUuid, + characteristicPQUuid, + object : BleNotifyCallback() { + override fun onNotifySuccess() { + Log.v(LOG_TAG, "Notify PQ Success") + //initNotificationSat() + } + + override fun onNotifyFailure(exception: BleException) { + Log.v(LOG_TAG, "BLE Notify PQ Failure $exception") + //initNotificationSat() + } + + @UseExperimental(ExperimentalUnsignedTypes::class) + override fun onCharacteristicChanged(data: ByteArray) { + Log.v(LOG_TAG, "Notify PQ Characteristic changed") + val buffer = ByteBuffer.wrap(data) + buffer.order(ByteOrder.LITTLE_ENDIAN) + //val flags = buffer.short + val sat_view: Byte = buffer.get().toUByte().toByte() + val sat_used: Byte = buffer.get().toUByte().toByte() + val ehpe: Float = buffer.int.toUInt().toFloat()/100.0f + val evpe: Float = buffer.int.toUInt().toFloat()/100.0f + val hdop: Float = buffer.get().toUByte().toByte()*2/10.0f + val vdop: Float = buffer.get().toUByte().toByte()*2/10.0f + val elevation_gps: Double = buffer.int/100.0 + Log.v(LOG_TAG, "PQ $sat_view $sat_used $ehpe $evpe $hdop $vdop $elevation_gps") + LocalBroadcastManager.getInstance(baseContext).sendBroadcast( + Intent(ServicesConstants.BROADCAST_FILTER.FILTERQUALITY).putExtra(ServicesConstants.BROADCAST_KEY.KEYQUALITYSATVIEW, sat_view) + .putExtra(ServicesConstants.BROADCAST_KEY.KEYQUALITYSATUSED, sat_used) + .putExtra(ServicesConstants.BROADCAST_KEY.KEYQUALITYEHPE, ehpe) + .putExtra(ServicesConstants.BROADCAST_KEY.KEYQUALITYEVPE, evpe) + .putExtra(ServicesConstants.BROADCAST_KEY.KEYQUALITYHDOP, hdop) + .putExtra(ServicesConstants.BROADCAST_KEY.KEYQUALITYVDOP, vdop) + .putExtra(ServicesConstants.BROADCAST_KEY.KEYQUALITYELEGPS, elevation_gps) + ) + } + }) + } + + private fun initNotificationSat() { + BleManager.getInstance().notify( + bleDevice, + serviceWriteUuid, + characteristicSatUuid, + object : BleNotifyCallback() { + override fun onNotifySuccess() { + Log.v(LOG_TAG, "Notify Sat Success") + //initNotificationSat() + } + + override fun onNotifyFailure(exception: BleException) { + Log.v(LOG_TAG, "BLE Notify Sat Failure $exception") + //initNotificationSat() + } + + override fun onCharacteristicChanged(data: ByteArray) { + Log.v(LOG_TAG, "Notify Sat Characteristic changed") + val buffer = ByteBuffer.wrap(data) + buffer.order(ByteOrder.LITTLE_ENDIAN) + LocalBroadcastManager.getInstance(baseContext).sendBroadcast( + Intent(ServicesConstants.BROADCAST_FILTER.FILTERSAT).putExtra(ServicesConstants.BROADCAST_KEY.KEYSATGPSALL, buffer.get()) + .putExtra(ServicesConstants.BROADCAST_KEY.KEYSATGPSUSED, buffer.get()) + .putExtra(ServicesConstants.BROADCAST_KEY.KEYSATSBASALL, buffer.get()) + .putExtra(ServicesConstants.BROADCAST_KEY.KEYSATSBASUSED, buffer.get()) + .putExtra(ServicesConstants.BROADCAST_KEY.KEYSATGLONASSALL, buffer.get()) + .putExtra(ServicesConstants.BROADCAST_KEY.KEYSATGLONASSUSED, buffer.get()) + .putExtra(ServicesConstants.BROADCAST_KEY.KEYSATGALILEOALL, buffer.get()) + .putExtra(ServicesConstants.BROADCAST_KEY.KEYSATGALILEOUSED, buffer.get()) + ) + } + }) + } + + fun disconnectBLE() + { + if(connected) { + LocalBroadcastManager.getInstance(baseContext).sendBroadcast( + Intent(ServicesConstants.BROADCAST_FILTER.FILTERBT).putExtra(ServicesConstants.BROADCAST_KEY.KEYBTSTATUS, false) + ) + connected = false + + + BleManager.getInstance().stopNotify( + bleDevice, + serviceEnvironmentalSensingUuid, + characteristicTemperatureUuid + ) + BleManager.getInstance() + .stopNotify(bleDevice, serviceEnvironmentalSensingUuid, characteristicPressureUuid) + BleManager.getInstance().stopNotify(bleDevice, serviceLNUuid, characteristicLSUuid) + BleManager.getInstance().stopNotify(bleDevice, serviceLNUuid, characteristicPQUuid) + BleManager.getInstance().stopNotify(bleDevice, serviceWriteUuid, characteristicSatUuid) + BleManager.getInstance().disconnect(bleDevice) + //BleManager.getInstance().destroy() + } + } + + fun sendCalibrate(data: ByteArray) { + if(connected) { + BleManager.getInstance().write( + bleDevice, + serviceWriteUuid, + characteristicCalibrateUuid, + data, + object : BleWriteCallback() { + override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray) { + Log.v(LOG_TAG, "Conf sended") + } + + override fun onWriteFailure(exception: BleException) { + Log.v(LOG_TAG, "Conf send Failure $exception") + } + }) + } + } + fun sendConf(data: ByteArray) { + if (connected) { + BleManager.getInstance().write( + bleDevice, + serviceWriteUuid, + characteristicWriteUuid, + data, + object : BleWriteCallback() { + override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray) { + Log.v(LOG_TAG, "Conf sended") + } + + override fun onWriteFailure(exception: BleException) { + Log.v(LOG_TAG, "Conf send Failure $exception") + } + }) + } + } + /*fun registerBMP() + { + initNotificationPressure() + initNotificationTemperature() + }*/ +} \ No newline at end of file diff --git a/app/src/main/java/net/nigreon/blegps/GPSConfiguration.kt b/app/src/main/java/net/nigreon/blegps/GPSConfiguration.kt new file mode 100644 index 0000000..2968ce3 --- /dev/null +++ b/app/src/main/java/net/nigreon/blegps/GPSConfiguration.kt @@ -0,0 +1,16 @@ +package net.nigreon.blegps + +class GPSConfiguration { + var bmpNrf: Boolean = false + var bmpBT: Boolean = false + var bmp2gps: Boolean = false + var extTemp: Boolean = false + var gpsBLE: Boolean = false + var gpsVerbose: Boolean = false + var gpsSerial: Boolean = false + var gpsMode: Int = 0 + var gpsBoost: Boolean = false + var gpsPollerSend: Short = 0 + var gpsSendBTSend: Short = 0 + var bmpPollerSend: Short = 0 +} \ No newline at end of file diff --git a/app/src/main/java/net/nigreon/blegps/MainActivity.kt b/app/src/main/java/net/nigreon/blegps/MainActivity.kt new file mode 100644 index 0000000..9c9f831 --- /dev/null +++ b/app/src/main/java/net/nigreon/blegps/MainActivity.kt @@ -0,0 +1,614 @@ +package net.nigreon.blegps + +import android.support.v7.app.AppCompatActivity +import android.os.Bundle +import kotlinx.android.synthetic.main.activity_main.* +import android.content.Intent +import android.os.IBinder +import android.content.ComponentName +import android.content.Context +import android.content.ServiceConnection +import android.support.v4.content.LocalBroadcastManager +import android.content.BroadcastReceiver +import android.content.IntentFilter +import android.os.Handler +import android.util.Log +import android.view.View +import android.widget.AdapterView +import android.location.Location +import com.clj.fastble.BleManager +import java.text.SimpleDateFormat +import java.util.* +import android.content.SharedPreferences +import android.os.Environment +import java.io.File +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException + +//https://stackoverflow.com/questions/1625249/android-how-to-bind-spinner-to-custom-object-list + +class MainActivity : AppCompatActivity() { + + private val LOG_TAG = "MainActivity" + + private lateinit var mBoundService: BLEMockLocationService + var mServiceBound = false + + private lateinit var mBoundServiceHR: BLEHRService + var mServiceBoundHR = false + + var currentPressure: Int = -1 + var currentTemperature: Float = -1.0f + var BTConnected = false + var BTHRConnected = false + var lastLocation: Location = Location("LastLocation") + + private lateinit var gpxfilename: String + private var gpxopened = false + + private lateinit var sharedPreferences: SharedPreferences + + + /*override fun onStart() { + super.onStart() + // Bind to LocalService + + }*/ + + fun delayFunction(function: ()-> Unit, delay: Long) { + Handler().postDelayed(function, delay) + } + + fun disconnectBT() + { + Intent(this, BLEMockLocationService::class.java).also { intent -> + intent.action = ServicesConstants.MOCKSERVICEACTION.STOPFOREGROUND_ACTION + startService(intent) + } + unbindService(mServiceConnection) + mServiceBound = false + } + + fun disconnectBTHR() + { + Intent(this, BLEHRService::class.java).also { intent -> + intent.action = ServicesConstants.HRSERVICEACTION.STOPFOREGROUND_ACTION + startService(intent) + } + unbindService(mServiceConnectionHR) + mServiceBoundHR = false + } + + fun filter01changed(intent: Intent) + { + Log.v(LOG_TAG, "Filter01 Broadcast received") + text01.text=intent.getIntExtra(ServicesConstants.BROADCAST_KEY.KEY01,-1).toString() + + } + fun temperatureChanged(intent: Intent) + { + Log.v(LOG_TAG, "Temperature Broadcast received") + currentTemperature = intent.getFloatExtra(ServicesConstants.BROADCAST_KEY.KEYTEMP, -1.0f) + writeBMPText() + } + fun pressureChanged(intent: Intent) + { + Log.v(LOG_TAG, "Pressure Broadcast received") + currentPressure = intent.getIntExtra(ServicesConstants.BROADCAST_KEY.KEYPRESSURE,-1) + writeBMPText() + } + + fun BTChanged(intent: Intent) + { + val connected = intent.getBooleanExtra(ServicesConstants.BROADCAST_KEY.KEYBTSTATUS,false) + if(connected) { + BTConnected=true + text01.text = "BT Connected" + Log.v(LOG_TAG, "BT Connected Broadcast received") + mBoundService.HRStatusChanged(BTHRConnected) + sendConfToBLEService() + if(swlog2file.isChecked()) + { + mBoundService.openGPX(gpxfilename) + gpxopened = true + } + } else { + text01.text = "BT Disconnected" + Log.v(LOG_TAG, "BT Disconnected Broadcast received") + BTConnected=false + swblepower.setChecked(false) + } + writeBMPText() + } + + fun BTChangedHR(intent: Intent) + { + val connected = intent.getBooleanExtra(ServicesConstants.BROADCAST_KEY.KEYBTSTATUSHR,false) + if(connected) { + BTHRConnected=true + text01.text = "BT HR Connected" + Log.v(LOG_TAG, "BT HR Connected Broadcast received") + if(BTConnected) { mBoundService.HRStatusChanged(true) } + } else { + text01.text = "BT HR Disconnected" + Log.v(LOG_TAG, "BT HR Disconnected Broadcast received") + BTHRConnected=false + if(BTConnected) { mBoundService.HRStatusChanged(false) } + } + writeBMPText() + } + + fun BTChangedHRM(intent: Intent) + { + texthr.text = intent.getByteExtra(ServicesConstants.BROADCAST_KEY.KEYBTHRM,0).toString() + " bpm" + } + + private fun convertLatLon(latitude: Double, longitude: Double): String { + val builder = StringBuilder() + + + val latitudeDegrees = Location.convert(Math.abs(latitude), Location.FORMAT_MINUTES) + //val latitudeSplit1 = latitudeDegrees.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + if (latitude < 0) { + builder.append("S ") + } else { + builder.append("N ") + } + val latitudeSplit1 = latitudeDegrees.split(':') + builder.append(latitudeSplit1[0]) + builder.append("°") + val latitudeSplit2 = latitudeSplit1[1].split(',') + builder.append(latitudeSplit2[0]) + builder.append(".") + builder.append(latitudeSplit2[1]) + builder.append(" ") + + + val longitudeDegrees = Location.convert(Math.abs(longitude), Location.FORMAT_MINUTES) + if (longitude < 0) { + builder.append("W ") + } else { + builder.append("E ") + } + val longitudeSplit1 = longitudeDegrees.split(':') + builder.append(longitudeSplit1[0]) + builder.append("°") + val longitudeSplit2 = longitudeSplit1[1].split(',') + builder.append(longitudeSplit2[0]) + builder.append(".") + builder.append(longitudeSplit2[1]) + + + return builder.toString() + } + + fun writeBMPText() + { + textbmp.text = "${currentPressure/100.0f} hPa $currentTemperature°C" + } + + fun writeLocationText(intent: Intent) + { + val currentLatitude = intent.getDoubleExtra(ServicesConstants.BROADCAST_KEY.KEYLOCATIONLAT,-1.0) + val currentLongitude = intent.getDoubleExtra(ServicesConstants.BROADCAST_KEY.KEYLOCATIONLON,-1.0) + val currentEHPE = intent.getFloatExtra(ServicesConstants.BROADCAST_KEY.KEYLOCATIONEHPE,-1.0f) + val currentElevation = intent.getDoubleExtra(ServicesConstants.BROADCAST_KEY.KEYLOCATIONELE,-1.0) + val currentSpeed = (intent.getFloatExtra(ServicesConstants.BROADCAST_KEY.KEYLOCATIONSPEED,-1.0f))/1000.0f*3600.0f + val currentHeading = intent.getFloatExtra(ServicesConstants.BROADCAST_KEY.KEYLOCATIONHEADING,-1.0f) + var currentLocation: Location = Location("CurrentLocation") + currentLocation.latitude = currentLatitude + currentLocation.longitude = currentLongitude + + textlastfix.text = "${SimpleDateFormat("dd-MM-yyyy HH:mm:ss.SSS").format(Date())}" + textlocation.text = "${convertLatLon(currentLatitude, currentLongitude)} $currentElevation m \n $currentLatitude $currentLongitude $currentElevation m" + textspeedheading.text = "$currentSpeed km/h $currentHeading° ${currentLocation.distanceTo(lastLocation)} m" + lastLocation = currentLocation + + if(swblegpsverbose.isChecked == false) { + textquality.text = "$currentEHPE m" + textsatellites.text = "Satellites in verbose" + } + } + + fun writeQualityText(intent: Intent) + { + val currentEHPE = intent.getFloatExtra(ServicesConstants.BROADCAST_KEY.KEYQUALITYEHPE,-1.0f) + val currentEVPE = intent.getFloatExtra(ServicesConstants.BROADCAST_KEY.KEYQUALITYEVPE,-1.0f) + val currentHDOP = intent.getFloatExtra(ServicesConstants.BROADCAST_KEY.KEYQUALITYHDOP,-1.0f) + val currentVDOP = intent.getFloatExtra(ServicesConstants.BROADCAST_KEY.KEYQUALITYVDOP,-1.0f) + val sat_view = intent.getByteExtra(ServicesConstants.BROADCAST_KEY.KEYQUALITYSATVIEW,-1) + val sat_used = intent.getByteExtra(ServicesConstants.BROADCAST_KEY.KEYQUALITYSATUSED,-1) + val elevation_gps = intent.getDoubleExtra(ServicesConstants.BROADCAST_KEY.KEYQUALITYELEGPS,-1.0) + textquality.text = "$currentEHPE m $currentEVPE m $currentHDOP $currentVDOP $sat_used/$sat_view $elevation_gps m" + } + + fun writeSatText(intent: Intent) + { + val gpsall = intent.getByteExtra(ServicesConstants.BROADCAST_KEY.KEYSATGPSALL,-1) + val gpsused = intent.getByteExtra(ServicesConstants.BROADCAST_KEY.KEYSATGPSUSED,-1) + val sbasall = intent.getByteExtra(ServicesConstants.BROADCAST_KEY.KEYSATSBASALL,-1) + val sbasused = intent.getByteExtra(ServicesConstants.BROADCAST_KEY.KEYSATSBASUSED,-1) + val glonassall = intent.getByteExtra(ServicesConstants.BROADCAST_KEY.KEYSATGLONASSALL,-1) + val glonassused = intent.getByteExtra(ServicesConstants.BROADCAST_KEY.KEYSATGLONASSUSED,-1) + val galileoall = intent.getByteExtra(ServicesConstants.BROADCAST_KEY.KEYSATGALILEOALL,-1) + val galileoused = intent.getByteExtra(ServicesConstants.BROADCAST_KEY.KEYSATGALILEOUSED,-1) + textsatellites.text = "GPS: $gpsused/$gpsall SBAS: $sbasused/$sbasall GLONASS: $glonassused/$glonassall GALILEO: $galileoused/$galileoall" + } + private fun sendCalibrateToBLEService() + { + if(BTConnected) { + val calibrateval: Int = editcalibratebmp2gps.text.toString().toInt()*100 + mBoundService.sendCalibrateBLE(calibrateval) + } + } + private fun sendConfToBLEService() + { + if(BTConnected) { + val bmppollersend: Float = resources.getStringArray(R.array.pollerval)[spinbmp.selectedItemPosition].toFloat() * 4 + var gpspollersend: Float + var gpssendbtsend: Float + var gpsmode: Int + + /*if(swblegpsboost.isChecked) + { + gpspollersend = 4f + gpssendbtsend = 4f + gpsmode = 1 + } else {*/ + gpspollersend = resources.getStringArray(R.array.pollerval)[spingps.selectedItemPosition].toFloat() * 4 + gpssendbtsend = resources.getStringArray(R.array.pollerval)[spingpssendbt.selectedItemPosition].toFloat() * 4 + gpsmode = spingpsmode.selectedItemPosition + //} + + var gpsConf: GPSConfiguration = GPSConfiguration() + + gpsConf.bmpNrf = swbmpnrf.isChecked + gpsConf.bmpBT = swbmpbt.isChecked + gpsConf.bmp2gps = swbmp2gps.isChecked + gpsConf.extTemp = swexttemp.isChecked + gpsConf.gpsBLE = swgpsble.isChecked + gpsConf.gpsVerbose = swblegpsverbose.isChecked + //gpsConf.gpsBoost = swblegpsboost.isChecked + gpsConf.gpsSerial = swgpsserial.isChecked + gpsConf.gpsMode = gpsmode + gpsConf.gpsPollerSend = gpspollersend.toShort() + gpsConf.gpsSendBTSend = gpssendbtsend.toShort() + gpsConf.bmpPollerSend = bmppollersend.toShort() + + mBoundService.sendConfBLE(gpsConf) + + } + } + private fun sendStopConfToBLEService() + { + if(BTConnected) { + val gpspollersend: Float = + resources.getStringArray(R.array.pollerval)[spingps.selectedItemPosition].toFloat() * 4 + val bmppollersend: Float = + resources.getStringArray(R.array.pollerval)[spinbmp.selectedItemPosition].toFloat() * 4 + val gpssendbtsend: Float = + resources.getStringArray(R.array.pollerval)[spingpssendbt.selectedItemPosition].toFloat() * 4 + + var gpsConf: GPSConfiguration = GPSConfiguration() + + gpsConf.bmpNrf = false + gpsConf.bmpBT = false + gpsConf.bmp2gps = false + gpsConf.extTemp = false + gpsConf.gpsBLE = false + gpsConf.gpsVerbose = false + gpsConf.gpsSerial = false + gpsConf.gpsMode = spingpsmode.selectedItemPosition + gpsConf.gpsPollerSend = gpspollersend.toShort() + gpsConf.gpsSendBTSend = gpssendbtsend.toShort() + gpsConf.bmpPollerSend = bmppollersend.toShort() + + mBoundService.sendConfBLE(gpsConf) + + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + BleManager.getInstance().init(application) + BleManager.getInstance() + .enableLog(true) + .setReConnectCount(1, 5000) + + BleManager.getInstance().operateTimeout = 5000 + + val brReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + + when (intent?.action) { + ServicesConstants.BROADCAST_FILTER.FILTER01 -> filter01changed(intent) + ServicesConstants.BROADCAST_FILTER.FILTERBT -> BTChanged(intent) + ServicesConstants.BROADCAST_FILTER.FILTERBTHR -> BTChangedHR(intent) + ServicesConstants.BROADCAST_FILTER.FILTERBTHRM -> BTChangedHRM(intent) + ServicesConstants.BROADCAST_FILTER.FILTERTEMP -> temperatureChanged(intent) + ServicesConstants.BROADCAST_FILTER.FILTERPRESSURE -> pressureChanged(intent) + ServicesConstants.BROADCAST_FILTER.FILTERLOCATION -> writeLocationText(intent) + ServicesConstants.BROADCAST_FILTER.FILTERQUALITY -> writeQualityText(intent) + ServicesConstants.BROADCAST_FILTER.FILTERSAT -> writeSatText(intent) + + //BROADCAST_CHANGE_TYPE_CHANGED -> handleChangeTypeChanged() + } + } + } + + spinbmp.setSelection(11) + spingps.setSelection(3) + spingpsmode.setSelection(1) + spingpssendbt.setSelection(10) + + lastLocation.latitude = 0.0 + lastLocation.longitude = 0.0 + + sharedPreferences = baseContext.getSharedPreferences("blegps_config", MODE_PRIVATE) + + /*sharedPreferences + .edit() + .putInt("inttest", 64) + .putString("stringtest", "24:6F:28:16:C1:F2") + .apply()*/ + + //text01.text = "MAC ${sharedPreferences.getString("stringtest", null)}" + + //mButton.setEnabled(false) + + /*buttonread.setOnClickListener { + //text01.text=edit01.text.toString() + BleManager.getInstance().read( + bleDevice, + serviceWriteUuid, + characteristicWriteUuid, + object : BleReadCallback() { + override fun onReadSuccess(data: ByteArray) { + text01.text = "Read OK" + textread.text = data.toString() + } + + override fun onReadFailure(exception: BleException) { + text01.text = "Read Failure $exception" + } + } + ) + } + buttonwrite.setOnClickListener { + val writeba: ByteArray = edit01.text.toString().toByteArray() + BleManager.getInstance().write( + bleDevice, + serviceWriteUuid, + characteristicWriteUuid, + writeba, + object : BleWriteCallback() { + override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray) { + text01.text = "Write OK" + } + + override fun onWriteFailure(exception: BleException) { + text01.text = "Write Failure $exception" + } + } + ) + }*/ + + swblepower.setOnCheckedChangeListener { _, isChecked -> + if(isChecked) { + Intent(this, BLEMockLocationService::class.java).also { intent -> + bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE) + intent.action = ServicesConstants.MOCKSERVICEACTION.STARTFOREGROUND_ACTION + startService(intent) + } + } else { + sendStopConfToBLEService() + delayFunction({ disconnectBT() }, 1500) + } + } + + swblehrpower.setOnCheckedChangeListener { _, isChecked -> + if(isChecked) { + Intent(this, BLEHRService::class.java).also { intent -> + bindService(intent, mServiceConnectionHR, Context.BIND_AUTO_CREATE) + intent.action = ServicesConstants.HRSERVICEACTION.STARTFOREGROUND_ACTION + startService(intent) + } + } else { + //sendStopConfToBLEService() + delayFunction({ disconnectBTHR() }, 1500) + } + } + + swbmpnrf.setOnCheckedChangeListener { _,_ -> + sendConfToBLEService() + } + swbmpbt.setOnCheckedChangeListener { _, checked -> + if(checked) { + LocalBroadcastManager.getInstance(baseContext).sendBroadcast( + Intent(ServicesConstants.BROADCAST_FILTER.FILTERBMP).putExtra(ServicesConstants.BROADCAST_KEY.KEYBMPSTATUS, true) + ) + } else { + LocalBroadcastManager.getInstance(baseContext).sendBroadcast( + Intent(ServicesConstants.BROADCAST_FILTER.FILTERBMP).putExtra(ServicesConstants.BROADCAST_KEY.KEYBMPSTATUS, false) + ) + } + sendConfToBLEService() + } + swbmp2gps.setOnCheckedChangeListener { _,_ -> + sendConfToBLEService() + } + swexttemp.setOnCheckedChangeListener { _,_ -> + sendConfToBLEService() + } + swgpsble.setOnCheckedChangeListener { _,_ -> + sendConfToBLEService() + } + swblegpsverbose.setOnCheckedChangeListener { _, _ -> + sendConfToBLEService() + } + swgpsserial.setOnCheckedChangeListener { _,_ -> + sendConfToBLEService() + } + /*swblegpsboost.setOnCheckedChangeListener { _,_ -> + sendConfToBLEService() + }*/ + swlog2file.setOnCheckedChangeListener { _, checked -> + if(checked) { + gpxfilename = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date()) + if (BTConnected) { + mBoundService.openGPX(gpxfilename) + gpxopened = true + } + } else { + if(BTConnected) { + mBoundService.closeGPX() + } else if(gpxopened) { + var gpxhandle = File( + Environment.getExternalStorageDirectory().path + File.separator + "BTGPS", + gpxfilename + ".gpx" + ) + var gpxoutputstream = FileOutputStream(gpxhandle, true) + try { + val gpxfooter = "\n" + gpxoutputstream.write(gpxfooter.toByteArray()) + } catch (e: FileNotFoundException) { + e.printStackTrace() + } catch (e: IOException) { + e.printStackTrace() + } + gpxoutputstream.close() + } + gpxopened = false + } + //sendConfToBLEService() + } + + spingpsmode.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onNothingSelected(p0: AdapterView<*>?) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) { + sendConfToBLEService() + } + + } + + spinbmp.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onNothingSelected(p0: AdapterView<*>?) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) { + sendConfToBLEService() + } + + } + + spingps.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onNothingSelected(p0: AdapterView<*>?) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) { + sendConfToBLEService() + } + + } + spingpssendbt.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onNothingSelected(p0: AdapterView<*>?) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) { + sendConfToBLEService() + } + + } + + buttoncalibratebmp2gps.setOnClickListener { + //text01.text = editcalibratebmp2gps.text + sendCalibrateToBLEService() + } + + /*startmock.setOnClickListener { + //val startIntent = Intent(this,BLEMockLocationService::class.java) + //startIntent.action = ServicesConstants.MOCKSERVICEACTION.STARTFOREGROUND_ACTION + //startService(startIntent) + Intent(this, BLEMockLocationService::class.java).also { intent -> + bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE) + intent.action = ServicesConstants.MOCKSERVICEACTION.STARTFOREGROUND_ACTION + startService(intent) + } + } + stopmock.setOnClickListener { + //val stopIntent = Intent(this,BLEMockLocationService::class.java) + //stopIntent.action = ServicesConstants.MOCKSERVICEACTION.STOPFOREGROUND_ACTION + //startService(stopIntent) + //LocalBroadcastManager.getInstance(this) + // .unregisterReceiver(brReceiver) + Intent(this, BLEMockLocationService::class.java).also { intent -> + intent.action = ServicesConstants.MOCKSERVICEACTION.STOPFOREGROUND_ACTION + startService(intent) + } + unbindService(mServiceConnection) + mServiceBound = false + + }*/ + /*bCounter.setOnClickListener { + textread.text=mBoundService.getCounter().toString() + }*/ + + + val manager = LocalBroadcastManager.getInstance(this) + manager.registerReceiver(brReceiver, IntentFilter(ServicesConstants.BROADCAST_FILTER.FILTER01)) + manager.registerReceiver(brReceiver, IntentFilter(ServicesConstants.BROADCAST_FILTER.FILTERBT)) + manager.registerReceiver(brReceiver, IntentFilter(ServicesConstants.BROADCAST_FILTER.FILTERBTHR)) + manager.registerReceiver(brReceiver, IntentFilter(ServicesConstants.BROADCAST_FILTER.FILTERBTHRM)) + manager.registerReceiver(brReceiver, IntentFilter(ServicesConstants.BROADCAST_FILTER.FILTERTEMP)) + manager.registerReceiver(brReceiver, IntentFilter(ServicesConstants.BROADCAST_FILTER.FILTERPRESSURE)) + manager.registerReceiver(brReceiver, IntentFilter(ServicesConstants.BROADCAST_FILTER.FILTERLOCATION)) + manager.registerReceiver(brReceiver, IntentFilter(ServicesConstants.BROADCAST_FILTER.FILTERQUALITY)) + manager.registerReceiver(brReceiver, IntentFilter(ServicesConstants.BROADCAST_FILTER.FILTERSAT)) + + //LocalBroadcastManager.getInstance(this) + // .unregisterReceiver(broadCastReceiver) + } + + override fun onPause() { + super.onPause() + if(!swlog2file.isChecked) { + swblegpsverbose.setChecked(false) + } + } + + private val mServiceConnection = object : ServiceConnection { + + override fun onServiceDisconnected(name: ComponentName) { + mServiceBound = false + } + + override fun onServiceConnected(name: ComponentName, service: IBinder) { + val binder = service as BLEMockLocationService.sBinder + mBoundService = binder.getService() + mServiceBound = true + } + } + + private val mServiceConnectionHR = object : ServiceConnection { + + override fun onServiceDisconnected(name: ComponentName) { + mServiceBoundHR = false + } + + override fun onServiceConnected(name: ComponentName, service: IBinder) { + val binderHR = service as BLEHRService.sBinder + mBoundServiceHR = binderHR.getService() + mServiceBoundHR = true + } + } + +} diff --git a/app/src/main/java/net/nigreon/blegps/MockLocationProvider.kt b/app/src/main/java/net/nigreon/blegps/MockLocationProvider.kt new file mode 100644 index 0000000..7fa3f19 --- /dev/null +++ b/app/src/main/java/net/nigreon/blegps/MockLocationProvider.kt @@ -0,0 +1,40 @@ +package net.nigreon.blegps + +import android.content.Context +import android.location.Location +import android.location.LocationManager +import android.os.SystemClock + +class MockLocationProvider(val providerName: String, val ctx: Context) { + + private val lm = ctx.getSystemService( + Context.LOCATION_SERVICE + ) as LocationManager + + fun start() { + + lm.addTestProvider( + providerName, false, false, false, false, true, + true, true, 0, 5 + ) + lm.setTestProviderEnabled(providerName, true) + } + + fun pushLocation(lat: Double, lon: Double, alt: Double, speed: Float, heading: Float, accuracy: Float) { + val mockLocation = Location(providerName) + mockLocation.latitude = lat + mockLocation.longitude = lon + mockLocation.altitude = alt + mockLocation.speed = speed + mockLocation.bearing = heading + mockLocation.time = System.currentTimeMillis() + mockLocation.accuracy = accuracy + mockLocation.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() + + lm.setTestProviderLocation(providerName, mockLocation) + } + + fun shutdown() { + lm.removeTestProvider(providerName) + } +} \ No newline at end of file diff --git a/app/src/main/java/net/nigreon/blegps/ServicesConstants.kt b/app/src/main/java/net/nigreon/blegps/ServicesConstants.kt new file mode 100644 index 0000000..4ae0109 --- /dev/null +++ b/app/src/main/java/net/nigreon/blegps/ServicesConstants.kt @@ -0,0 +1,73 @@ +package net.nigreon.blegps + +class ServicesConstants { + interface MOCKSERVICEACTION { + companion object { + val MAIN_ACTION = "net.nigreon.blegps.mockservice.action.main" + val STARTFOREGROUND_ACTION = "net.nigreon.blegps.mockservice.action.startforeground" + val STOPFOREGROUND_ACTION = "net.nigreon.blegps.mockservice.action.stopforeground" + val TASKER_ACTION = "net.nigreon.blegps.mockservice.action.tasker" + } + } + interface HRSERVICEACTION { + companion object { + //val MAIN_ACTION = "net.nigreon.blegps.mockservice.action.main" + val STARTFOREGROUND_ACTION = "net.nigreon.blegps.hrservice.action.startforeground" + val STOPFOREGROUND_ACTION = "net.nigreon.blegps.hrservice.action.stopforeground" + } + } + interface NOTIFICATION_ID { + companion object { + val FOREGROUND_MOCK_SERVICE = 101 + val FOREGROUND_HR_SERVICE = 102 + } + } + interface BROADCAST_FILTER { + companion object { + const val FILTER01 = "just.a.filter" + const val FILTERBT = "filter.bt" + const val FILTERBMP = "filter.bmp" + const val FILTERBTHR = "filter.bthr" + const val FILTERBTHRM = "filter.bthrm" + const val FILTERTEMP = "filter.temp" + const val FILTERPRESSURE = "filter.pressure" + const val FILTERLOCATION = "filter.location" + const val FILTERQUALITY = "filter.quality" + const val FILTERSAT = "filter.sat" + const val FILTERTASKER= "filter.tasker" + } + } + interface BROADCAST_KEY { + companion object { + const val KEY01 = "key01" + const val KEYBTSTATUS = "btstatus" + const val KEYBTSTATUSHR = "btstatushr" + const val KEYBTHRM = "bthrm" + const val KEYBMPSTATUS = "bmpstatus" + const val KEYTEMP = "temperature" + const val KEYPRESSURE = "pressure" + const val KEYBOOST = "boost" + const val KEYLOCATIONLAT = "latitude" + const val KEYLOCATIONLON = "longitude" + const val KEYLOCATIONELE = "elevation" + const val KEYLOCATIONSPEED = "speed" + const val KEYLOCATIONHEADING = "heading" + const val KEYLOCATIONEHPE = "lehpe" + const val KEYQUALITYSATVIEW = "satview" + const val KEYQUALITYSATUSED = "satused" + const val KEYQUALITYEHPE = "ehpe" + const val KEYQUALITYEVPE = "evpe" + const val KEYQUALITYHDOP = "hdop" + const val KEYQUALITYVDOP = "vdop" + const val KEYQUALITYELEGPS = "elevation_gps" + const val KEYSATGPSALL = "gpsall" + const val KEYSATGPSUSED = "gpsused" + const val KEYSATSBASALL = "sbasall" + const val KEYSATSBASUSED = "sbasused" + const val KEYSATGLONASSALL = "glonassall" + const val KEYSATGLONASSUSED = "glonassused" + const val KEYSATGALILEOALL = "galileoall" + const val KEYSATGALILEOUSED = "galileoused" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/net/nigreon/blegps/TaskerReceiver.kt b/app/src/main/java/net/nigreon/blegps/TaskerReceiver.kt new file mode 100644 index 0000000..4aff95c --- /dev/null +++ b/app/src/main/java/net/nigreon/blegps/TaskerReceiver.kt @@ -0,0 +1,23 @@ +package net.nigreon.blegps + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.support.v4.content.ContextCompat +import android.support.v4.content.LocalBroadcastManager +import android.util.Log + +class TaskerReceiver: BroadcastReceiver() { + private val LOG_TAG = "TaskerReceiver" + override fun onReceive(context: Context, intent: Intent) { + Log.v(LOG_TAG, "Receive") + val serviceIntent = Intent(context, BLEMockLocationService::class.java) + serviceIntent.action=ServicesConstants.MOCKSERVICEACTION.TASKER_ACTION + + //serviceIntent.putExtras(Intent(ServicesConstants.BROADCAST_FILTER.FILTERTASKER).putExtra(ServicesConstants.BROADCAST_KEY.KEYBOOST, false)) + serviceIntent.putExtras(intent) + //LocalBroadcastManager.getInstance(context).sendBroadcast( + // Intent(ServicesConstants.BROADCAST_FILTER.FILTERTASKER).putExtra(ServicesConstants.BROADCAST_KEY.KEYBOOST, true)) + ContextCompat.startForegroundService(context, serviceIntent) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..1f6bb29 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..0d025f9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..94fddf9 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,338 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +