commit 09b3ec6d68e957a8f2c49a75ce30bb1ae6b486c8 Author: JPaehr Date: Sat Sep 23 19:09:54 2023 +0200 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..bf10110 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,44 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + compileSdk 33 + + defaultConfig { + applicationId "com.example.oilcheckkotlin" + minSdk 28 + targetSdk 31 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.7.0' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.9.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + implementation 'com.luckycatlabs:SunriseSunsetCalculator:1.2' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /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 \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/oilcheckkotlin/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/example/oilcheckkotlin/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..d6b4461 --- /dev/null +++ b/app/src/androidTest/java/com/example/oilcheckkotlin/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.example.oilcheckkotlin + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.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.getInstrumentation().targetContext + assertEquals("com.example.oilcheckkotlin", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..14c9174 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/oilcheckkotlin/BLEConnectionService.kt b/app/src/main/java/com/example/oilcheckkotlin/BLEConnectionService.kt new file mode 100644 index 0000000..9ced612 --- /dev/null +++ b/app/src/main/java/com/example/oilcheckkotlin/BLEConnectionService.kt @@ -0,0 +1,209 @@ +package com.example.oilcheckkotlin + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.graphics.Color +import android.location.LocationManager +import android.os.Build +import android.os.IBinder +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.location.LocationManagerCompat + +class BLEConnectionService : Service() { + val CHANNEL_DEFAULT_IMPORTANCE = "1" + val ONGOING_NOTIFICATION_ID = 2 + var bleController: BLEController? = null + var deviceAddress: String? = null + var nlService = NLService() + private val myThread = Thread { + while (true){ + Log.d("Runnable", "Working") + if(bleController == null) { + + val lm = getSystemService(Context.LOCATION_SERVICE) as LocationManager + if (!LocationManagerCompat.isLocationEnabled(lm)) { + // Start Location Settings Activity, you should explain to the user why he need to enable location before. + startActivity(Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)) + } + Log.d("Debug", "bleController null") +// bleController = BLEController.getInstance(applicationContext) + bleController = BLEController(applicationContext) + + } else if(!bleController!!.isConnected()){ + // not connected + Log.d("Debug", "bleController not null but not connected") + bleController!!.init() + Log.d("Debug", "tried to init ble controller") + } else{ + // controller is connected + Log.d("Debug", "BLE device connected") + bleController!!.stopScanner() + + + } + Thread.sleep(12000) + } + } + override fun onCreate() { + super.onCreate() + + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + + if(intent?.action.equals("STARTService")){ + Log.d("DEBUG", "Startflag set") + myThread.start() + + // setup ble connection + val channel_id = + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){ + createNotificationChannel("my_service", "My Background Service") + } else { + "" + } + + val serviceIntent: PendingIntent = Intent(this, BLEConnectionService::class.java).let { notificationIntent -> + PendingIntent.getActivity(this, 0, notificationIntent, + PendingIntent.FLAG_IMMUTABLE) + } + val myIntent = Intent(this, MainActivity::class.java).apply { + var flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + + val contentIntent: PendingIntent = PendingIntent.getActivity(this, 0, myIntent, PendingIntent.FLAG_IMMUTABLE) + val notification: Notification = Notification.Builder(this, channel_id) + .setContentTitle("Oil-Check") + .setContentText("running") + .setSmallIcon(R.drawable.icon) + .setContentIntent(serviceIntent) +// .setTicker(getText(R.string.ticker_text)) + .build() + + startForeground(101, notification) + +// startService(Intent(this, NLService::class.java) +// .setAction("StartNLService") +// ) + + + val nlServiceIntent: PendingIntent = Intent(this, NLService::class.java).let { notificationIntent -> + PendingIntent.getActivity(this, 0, notificationIntent, + PendingIntent.FLAG_IMMUTABLE) + } + + val nlNotification: Notification = Notification.Builder(this, channel_id) + .setContentTitle("Oil-Check") + .setContentText("running") + .setSmallIcon(R.drawable.icon) + .setContentIntent(nlServiceIntent) +// .setTicker(getText(R.string.ticker_text)) + .build() + startForeground(101, nlNotification) + + + + + // setup notification listener +// val nlServiceIntent: PendingIntent = Intent(this, NLService::class.java).let { notificationIntent -> +// PendingIntent.getActivity(this, 0, notificationIntent, +// PendingIntent.FLAG_IMMUTABLE) +// } +// +// val nlNotification: Notification = Notification.Builder(this, channel_id) +// .setContentTitle("Oil-Check") +// .setContentText("running") +// .setSmallIcon(R.drawable.icon) +// .setContentIntent(nlServiceIntent) +//// .setTicker(getText(R.string.ticker_text)) +// .build() +// startForeground(101, nlNotification) + + + } else if (intent?.action.equals("STOPService")){ + Log.d("DEBUG", "Stopflag set") + stopForeground(true) + stopSelfResult(101) + stopSelf() + + } else if (intent?.action.equals("StartNLService")) { + // + } + else if(intent?.action.equals("Toggle")){ + try { + bleController!!.sendData("X".toByteArray(Charsets.UTF_8)) + } + catch (e: Exception){ + Log.d("Debug", "tried to toggle, but failed") + } + } else if(intent?.action.equals("X")){ + try { + bleController!!.sendData("X".toByteArray(Charsets.UTF_8)) + } + catch (e: Exception){ + Log.d("Debug", "Tried to send X, but connection failed") + } + } else if(intent?.action.equals("XX")){ + try { + bleController!!.sendData("XX".toByteArray(Charsets.UTF_8)) + } + catch (e: Exception){ + Log.d("Debug", "Tried to send XX, but connection failed") + } + } else if(intent?.action.equals("XXX")){ + try { + bleController!!.sendData("XXX".toByteArray(Charsets.UTF_8)) + } + catch (e: Exception){ + Log.d("Debug", "Tried to send XXX, but connection failed") + } + } else if(intent?.action.equals("XXXX")){ + try { + bleController!!.sendData("XXXX".toByteArray(Charsets.UTF_8)) + } + catch (e: Exception){ + Log.d("Debug", "Tried to send XXXX, but connection failed") + } + } else if(intent?.action.equals("OTA")) { + try { + bleController!!.sendData("U".toByteArray(Charsets.UTF_8)) + } + catch (e: Exception){ + Log.d("Debug", "Tried to update firmware, but connection failed") + } + } + else{ + Log.d("Debug", "Intent without arguments occured in service") + } + + + return START_STICKY + } + @RequiresApi(Build.VERSION_CODES.O) + private fun createNotificationChannel(channelId: String, channelName: String): String{ + val chan = NotificationChannel(channelId, + channelName, NotificationManager.IMPORTANCE_NONE) + chan.lightColor = Color.BLUE + chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE + val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + service.createNotificationChannel(chan) + return channelId + } + override fun onBind(p0: Intent?): IBinder? { +// TODO("Not yet implemented") + return null; + } + + override fun onDestroy() { + super.onDestroy() + this.myThread.interrupt() + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/oilcheckkotlin/BLEController.kt b/app/src/main/java/com/example/oilcheckkotlin/BLEController.kt new file mode 100644 index 0000000..1aa9d88 --- /dev/null +++ b/app/src/main/java/com/example/oilcheckkotlin/BLEController.kt @@ -0,0 +1,338 @@ +package com.example.oilcheckkotlin + +import android.Manifest +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCallback +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothManager +import android.bluetooth.BluetoothProfile +import android.bluetooth.le.BluetoothLeScanner +import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanResult +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.util.Log +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.app.ActivityCompat +import java.util.Locale + +class BLEController(ctx: Context) : BLEControllerListener{ + companion object{ + var instance: BLEController? = null + fun getInstance(ctx: Context?): BLEController? { + Log.d("BLEController", "called") + if (null == instance){ + instance = BLEController(ctx!!) + } + return instance + } + } + private val SCAN_PERIOD: Long = 10000 + + private var deviceAddress: String? = null + + var ctx: Context + private var devices = hashMapOf() + private var scanner: BluetoothLeScanner? = null + var bluetoothManager: BluetoothManager? = null + private var btGattChar: BluetoothGattCharacteristic? = null + private lateinit var bluetoothGatt: BluetoothGatt + private var device: BluetoothDevice? = null + private val listeners: ArrayList = ArrayList() + private var connectionState = false + + fun stopScanner(){ + if (ActivityCompat.checkSelfPermission( + ctx, + Manifest.permission.BLUETOOTH_SCAN + ) != PackageManager.PERMISSION_GRANTED + ) { + // TODO: Consider calling + // ActivityCompat#requestPermissions + // here to request the missing permissions, and then overriding + // public void onRequestPermissionsResult(int requestCode, String[] permissions, + // int[] grantResults) + // to handle the case where the user grants the permission. See the documentation + // for ActivityCompat#requestPermissions for more details. + return + } + try { + scanner!!.stopScan(bleCallback) + } + catch (e: Exception){ + Log.d("Debug", "Scanner seems to run, stopping") + } + } + + // constructor + init { + Log.d("Debug", "constructor called") + this.bluetoothManager = ctx.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager + this.ctx = ctx + } + + fun init(){ + Log.d("Debug", "init") + this.devices?.clear() + Log.d("Debug", "get scanner") + this.scanner = this.bluetoothManager?.adapter?.bluetoothLeScanner + Log.d("Debug", "got scanner") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (ActivityCompat.checkSelfPermission( + ctx, + Manifest.permission.BLUETOOTH_SCAN + ) != PackageManager.PERMISSION_GRANTED + ) { +// val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) +// requestBluetooth.launch(enableBtIntent) + Log.d("Debug", "permission needed") + // TODO: Consider calling + // ActivityCompat#requestPermissions + // here to request the missing permissions, and then overriding + // public void onRequestPermissionsResult(int requestCode, String[] permissions, + // int[] grantResults) + // to handle the case where the user grants the permission. See the documentation + // for ActivityCompat#requestPermissions for more details. + return + } else{ + Log.d("Debug", "permission granted") + } + } + + Log.d("Debug", "try to scan") + + this.scanner?.startScan(bleCallback) + + } + var bleCallback = object: ScanCallback(){ + + override fun onScanResult(callbackType: Int, result: ScanResult?) { +// Log.d("Debug", "onScanResult called") + val device = result?.device +// Log.d("Debug", "device result exists") + if (!devices?.containsKey(device?.address)!! && isThisTheDevice(device)) { + Log.d("Debug", "found the device") + deviceFound(device!!) + Log.d("Debug", device.address) + + + } + } + + override fun onBatchScanResults(results: List?) { + Log.d("Debug", "onBatchScanResults called") + for (sr in results!!) { + val device = sr?.device + if (!devices!!.containsKey(device!!.address) && isThisTheDevice(device)) { + deviceFound(device!!) + } + } + } + + override fun onScanFailed(errorCode: Int) { + Log.i("[BLE]", "scan failed with errorcode: $errorCode") + } + } + private fun isThisTheDevice(device: BluetoothDevice?): Boolean { + try { + if (ActivityCompat.checkSelfPermission( + ctx, + Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + // TODO: Consider calling + // ActivityCompat#requestPermissions + // here to request the missing permissions, and then overriding + // public void onRequestPermissionsResult(int requestCode, String[] permissions, + // int[] grantResults) + // to handle the case where the user grants the permission. See the documentation + // for ActivityCompat#requestPermissions for more details. + return false + } + Log.d("BLE", device!!.name) + } catch (e: Exception) { + Log.d("BLE", "Device with null-Name found") + } + return null != device!!.name && device.name.startsWith("OilCheck") + } + private fun deviceFound(device: BluetoothDevice) { + Log.d("Debug", "deviceFound called") +// connectToDevice(device.address) + devices.put(device.address, device) + fireDeviceFound(device) + if (ActivityCompat.checkSelfPermission( + ctx, + Manifest.permission.BLUETOOTH_SCAN + ) != PackageManager.PERMISSION_GRANTED + ) { + // TODO: Consider calling + // ActivityCompat#requestPermissions + // here to request the missing permissions, and then overriding + // public void onRequestPermissionsResult(int requestCode, String[] permissions, + // int[] grantResults) + // to handle the case where the user grants the permission. See the documentation + // for ActivityCompat#requestPermissions for more details. + return + } + scanner!!.stopScan(bleCallback) + } + private fun fireDeviceFound(device: BluetoothDevice) { +// for (l in listeners) { + if (ActivityCompat.checkSelfPermission( + ctx, + Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + // TODO: Consider calling + // ActivityCompat#requestPermissions + // here to request the missing permissions, and then overriding + // public void onRequestPermissionsResult(int requestCode, String[] permissions, + // int[] grantResults) + // to handle the case where the user grants the permission. See the documentation + // for ActivityCompat#requestPermissions for more details. + return + } + Log.d("Debug", "BLEDeviceFound called") + BLEDeviceFound(device.name.trim { it <= ' ' }, device.address) +// } + } + fun connectToDevice(address: String?) { + Log.d("Debug", "connectToDevice") + this.device = devices[address!!] + if (ActivityCompat.checkSelfPermission( + ctx, + Manifest.permission.BLUETOOTH_SCAN + ) != PackageManager.PERMISSION_GRANTED + ) { + // TODO: Consider calling + // ActivityCompat#requestPermissions + // here to request the missing permissions, and then overriding + // public void onRequestPermissionsResult(int requestCode, String[] permissions, + // int[] grantResults) + // to handle the case where the user grants the permission. See the documentation + // for ActivityCompat#requestPermissions for more details. + return + } + scanner!!.stopScan(bleCallback) + try { + Log.i("[BLE]", "connect to device " + device!!.getAddress()) + } catch (e: java.lang.Exception) { + Log.d("Debug", "Connection to device failed :(") + } + this.bluetoothGatt = device!!.connectGatt(null, true, this.bleConnectCallback) + } + + private val bleConnectCallback: BluetoothGattCallback = object : BluetoothGattCallback() { + val CHANNEL_ID = "ForegroundServiceChannel" + override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { + if (newState == BluetoothProfile.STATE_CONNECTED) { + connectionState = true + Log.d("Debug", "connection state changed to connected") + + if (ActivityCompat.checkSelfPermission( + ctx, + Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + // TODO: Consider calling + // ActivityCompat#requestPermissions + // here to request the missing permissions, and then overriding + // public void onRequestPermissionsResult(int requestCode, String[] permissions, + // int[] grantResults) + // to handle the case where the user grants the permission. See the documentation + // for ActivityCompat#requestPermissions for more details. + return + } + Log.i("[BLE]", "start service discovery " + bluetoothGatt.discoverServices()) + } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { + connectionState = false + + Log.d("Debug", "connection state changed to not connected") + btGattChar = null + Log.w("[BLE]", "DISCONNECTED with status $status") + fireDisconnected() + } else { + Log.i("[BLE]", "unknown state $newState and status $status") + } + } + + override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { + if (null == btGattChar) { + for (service in gatt.services) { + if (service.uuid.toString().uppercase(Locale.getDefault()) + .startsWith("00001811") + ) { + val gattCharacteristics = service.characteristics + for (bgc in gattCharacteristics) { + if (bgc.uuid.toString().uppercase(Locale.getDefault()) + .startsWith("00002A46") + ) { + val chprop = bgc.properties + if (chprop and BluetoothGattCharacteristic.PROPERTY_WRITE or (chprop and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) > 0) { + btGattChar = bgc + Log.i("[BLE]", "CONNECTED and ready to send") + fireConnected() + } + } + } + } + } + } + } + } + private fun fireDisconnected() { + for (l in listeners){ + l.BLEControllerDisconnected() + } + device = null + } + fun isConnected():Boolean { + if(!connectionState){ + return false + } + return true + } + fun sendData(data: ByteArray?) { + btGattChar!!.value = data + if (ActivityCompat.checkSelfPermission( + ctx, + Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + // TODO: Consider calling + // ActivityCompat#requestPermissions + // here to request the missing permissions, and then overriding + // public void onRequestPermissionsResult(int requestCode, String[] permissions, + // int[] grantResults) + // to handle the case where the user grants the permission. See the documentation + // for ActivityCompat#requestPermissions for more details. + return + } + bluetoothGatt.writeCharacteristic(btGattChar) + } + private fun fireConnected() { + for (l in listeners) { + l.BLEControllerConnected() + } + } + + override fun BLEControllerConnected() { + TODO("Not yet implemented") + } + + override fun BLEControllerDisconnected() { + TODO("Not yet implemented") + } + + override fun BLEDeviceFound(name: String?, address: String?) { +// log("Device $name found with address $address") + deviceAddress = address + Log.d("Debug", "connectToDevice called") +// btnConnect.setEnabled(true) + connectToDevice(deviceAddress) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/oilcheckkotlin/BLEControllerListener.kt b/app/src/main/java/com/example/oilcheckkotlin/BLEControllerListener.kt new file mode 100644 index 0000000..5f5d16a --- /dev/null +++ b/app/src/main/java/com/example/oilcheckkotlin/BLEControllerListener.kt @@ -0,0 +1,14 @@ +package com.example.oilcheckkotlin + +/* + * (c) Matey Nenov (https://www.thinker-talk.com) + * + * Licensed under Creative Commons: By Attribution 3.0 + * http://creativecommons.org/licenses/by/3.0/ + * + */ +interface BLEControllerListener { + fun BLEControllerConnected() + fun BLEControllerDisconnected() + fun BLEDeviceFound(name: String?, address: String?) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/oilcheckkotlin/MainActivity.kt b/app/src/main/java/com/example/oilcheckkotlin/MainActivity.kt new file mode 100644 index 0000000..ab2b6a4 --- /dev/null +++ b/app/src/main/java/com/example/oilcheckkotlin/MainActivity.kt @@ -0,0 +1,58 @@ +package com.example.oilcheckkotlin + +import android.app.Notification +import android.app.PendingIntent +import android.content.Intent +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import android.util.Log +import android.widget.Button +import com.luckycatlabs.sunrisesunset.SunriseSunsetCalculator +import com.luckycatlabs.sunrisesunset.dto.Location +import java.util.Calendar + +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + var location = Location("52.3759", "9.7320") + var calculator = SunriseSunsetCalculator(location, "GMT+0200") + + var sunrise = calculator.getOfficialSunsetForDate(Calendar.getInstance()) + + Log.d("Debug", sunrise) + + + findViewById