diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 282e60d0..19476689 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,7 +16,7 @@ android { minSdk = 26 targetSdk = 34 versionCode = 1 - versionName = "1.0.5" + versionName = "1.0.6" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/debug/app-debug.apk b/app/debug/app-debug.apk index 848aad53..2946ff47 100644 Binary files a/app/debug/app-debug.apk and b/app/debug/app-debug.apk differ diff --git a/app/debug/output-metadata.json b/app/debug/output-metadata.json index c2596b9a..11e041e7 100644 --- a/app/debug/output-metadata.json +++ b/app/debug/output-metadata.json @@ -12,7 +12,7 @@ "filters": [], "attributes": [], "versionCode": 1, - "versionName": "1.0.5", + "versionName": "1.0.6", "outputFile": "app-debug.apk" } ], diff --git a/app/release/app-release.apk b/app/release/app-release.apk index c486d6c6..f2d1177e 100644 Binary files a/app/release/app-release.apk and b/app/release/app-release.apk differ diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json index cd6e89ee..12ed0eee 100644 --- a/app/release/output-metadata.json +++ b/app/release/output-metadata.json @@ -12,7 +12,7 @@ "filters": [], "attributes": [], "versionCode": 1, - "versionName": "1.0.5", + "versionName": "1.0.6", "outputFile": "app-release.apk" } ], diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6cc533e9..fbdbda57 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -36,16 +36,22 @@ android:name="android.hardware.bluetooth_le" android:required="true" /> + + + + + + = mutableListOf() + private var _advertisementQueueHandlerCallbacks:MutableList = mutableListOf() + private var _active = false private var _advertisementQueueMode: AdvertisementQueueMode = AdvertisementQueueMode.ADVERTISEMENT_QUEUE_MODE_LINEAR @@ -30,6 +34,7 @@ class AdvertisementSetQueueHandler :IAdvertisementServiceCallback { private var _currentAdvertisementSetListIndex = 0 private var _currentAdvertisementSetIndex = 0 + init{ _advertisementService = AppContext.getAdvertisementService() if(_advertisementService != null){ @@ -69,7 +74,9 @@ class AdvertisementSetQueueHandler :IAdvertisementServiceCallback { } fun setAdvertisementSetCollection(advertisementSetCollection: AdvertisementSetCollection){ - _advertisementSetCollection = advertisementSetCollection + if(_advertisementSetCollection != advertisementSetCollection){ + _advertisementSetCollection = advertisementSetCollection + } // Reset indices _currentAdvertisementSet= null @@ -109,6 +116,17 @@ class AdvertisementSetQueueHandler :IAdvertisementServiceCallback { } } + fun addAdvertisementQueueHandlerCallback(callback: IAdvertisementSetQueueHandlerCallback){ + if(!_advertisementQueueHandlerCallbacks.contains(callback)){ + _advertisementQueueHandlerCallbacks.add(callback) + } + } + fun removeAdvertisementQueueHandlerCallback(callback: IAdvertisementSetQueueHandlerCallback){ + if(_advertisementQueueHandlerCallbacks.contains(callback)){ + _advertisementQueueHandlerCallbacks.remove(callback) + } + } + fun setIntervalSeconds(seconds:Int){ _interval = (seconds * 1000).toLong() } @@ -119,19 +137,48 @@ class AdvertisementSetQueueHandler :IAdvertisementServiceCallback { } } - fun activate(){ - _active = true - if(_currentAdvertisementSet != null){ - handleAdvertisementSet(_currentAdvertisementSet!!) - } else { - advertiseNextAdvertisementSet() + fun activate(startService: Boolean = true){ + if(!_active){ + _active = true + + if(startService){ + AdvertisementForegroundService.startService(AppContext.getContext(), "Foreground Service is running...") + } + + _advertisementQueueHandlerCallbacks.forEach { it -> + try { + it.onQueueHandlerActivated() + } catch (e:Exception){ + Log.e(_logTag, "Error while executing AdvertisementQueueHandlerCallback onQueueHandlerActivated") + } + } + + if(_currentAdvertisementSet != null){ + handleAdvertisementSet(_currentAdvertisementSet!!) + } else { + advertiseNextAdvertisementSet() + } } } - fun deactivate(){ + fun deactivate(stopService: Boolean = false){ _active = false - if(_advertisementService != null){ - _advertisementService!!.stopAdvertisement() + + if(AppContext.getAdvertisementService() != null){ + AppContext.getAdvertisementService().stopAdvertisement() + } + + if(stopService){ + Log.d(_logTag, "Stopping Foreground Service") + AdvertisementForegroundService.stopService(AppContext.getActivity()) + } + + _advertisementQueueHandlerCallbacks.forEach { it -> + try { + it.onQueueHandlerDeactivated() + } catch (e:Exception){ + Log.e(_logTag, "Error while executing AdvertisementQueueHandlerCallback onQueueHandlerDeactivated") + } } } @@ -287,13 +334,21 @@ class AdvertisementSetQueueHandler :IAdvertisementServiceCallback { // Callback Implementation, just pass to own Listeners override fun onAdvertisementSetStart(advertisementSet: AdvertisementSet?) { _advertisementServiceCallbacks.map { - it.onAdvertisementSetStart(advertisementSet) + try { + it.onAdvertisementSetStart(advertisementSet) + } catch (e:Exception){ + Log.e(_logTag, "Error in: onAdvertisementSetStart ${e.message}") + } } } override fun onAdvertisementSetStop(advertisementSet: AdvertisementSet?) { _advertisementServiceCallbacks.map { - it.onAdvertisementSetStop(advertisementSet) + try { + it.onAdvertisementSetStop(advertisementSet) + } catch (e:Exception){ + Log.e(_logTag, "Error in: onAdvertisementSetStop ${e.message}") + } } if(_advertisementService != null && !_advertisementService!!.isLegacyService()){ @@ -304,14 +359,22 @@ class AdvertisementSetQueueHandler :IAdvertisementServiceCallback { override fun onAdvertisementSetSucceeded(advertisementSet: AdvertisementSet?) { runLocalCallback(true) _advertisementServiceCallbacks.map { - it.onAdvertisementSetSucceeded(advertisementSet) + try { + it.onAdvertisementSetSucceeded(advertisementSet) + } catch (e:Exception){ + Log.e(_logTag, "Error in: onAdvertisementSetSucceeded ${e.message}") + } } } override fun onAdvertisementSetFailed(advertisementSet: AdvertisementSet?, advertisementError: AdvertisementError) { runLocalCallback(false) _advertisementServiceCallbacks.map { - it.onAdvertisementSetFailed(advertisementSet, advertisementError) + try { + it.onAdvertisementSetFailed(advertisementSet, advertisementError) + } catch (e:Exception){ + Log.e(_logTag, "Error in: onAdvertisementSetFailed ${e.message}") + } } } } \ No newline at end of file diff --git a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Interfaces/Callbacks/IAdvertisementSetQueueHandlerCallback.kt b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Interfaces/Callbacks/IAdvertisementSetQueueHandlerCallback.kt new file mode 100644 index 00000000..948dec0b --- /dev/null +++ b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Interfaces/Callbacks/IAdvertisementSetQueueHandlerCallback.kt @@ -0,0 +1,8 @@ +package de.simon.dankelmann.bluetoothlespam.Interfaces.Callbacks + +import de.simon.dankelmann.bluetoothlespam.Models.AdvertisementSet + +interface IAdvertisementSetQueueHandlerCallback { + fun onQueueHandlerActivated() + fun onQueueHandlerDeactivated() +} \ No newline at end of file diff --git a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/MainActivity.kt b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/MainActivity.kt index 323c381c..2948cb22 100644 --- a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/MainActivity.kt +++ b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/MainActivity.kt @@ -8,6 +8,7 @@ import android.content.Context import android.content.Intent import android.content.SharedPreferences.OnSharedPreferenceChangeListener import android.content.pm.PackageManager +import android.net.Uri import android.os.Build import android.os.Bundle import android.text.Spannable @@ -17,18 +18,17 @@ import android.util.Log import android.view.Menu import android.view.MenuItem import android.view.Window -import android.widget.Button import android.widget.SeekBar import android.widget.TextView import android.widget.Toast +import androidx.annotation.NonNull import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.core.content.ContextCompat +import androidx.core.view.GravityCompat import androidx.drawerlayout.widget.DrawerLayout -import androidx.navigation.Navigation.findNavController import androidx.navigation.findNavController import androidx.navigation.ui.AppBarConfiguration -import androidx.navigation.ui.NavigationUI import androidx.navigation.ui.NavigationUI.onNavDestinationSelected import androidx.navigation.ui.navigateUp import androidx.navigation.ui.setupActionBarWithNavController @@ -37,19 +37,12 @@ import androidx.preference.PreferenceManager import com.google.android.material.navigation.NavigationView import de.simon.dankelmann.bluetoothlespam.AppContext.AppContext import de.simon.dankelmann.bluetoothlespam.Constants.Constants -import de.simon.dankelmann.bluetoothlespam.Database.AppDatabase import de.simon.dankelmann.bluetoothlespam.Enums.TxPowerLevel import de.simon.dankelmann.bluetoothlespam.Helpers.BluetoothHelpers -import de.simon.dankelmann.bluetoothlespam.Helpers.DatabaseHelpers import de.simon.dankelmann.bluetoothlespam.Helpers.QueueHandlerHelpers -import de.simon.dankelmann.bluetoothlespam.Helpers.StringHelpers -import de.simon.dankelmann.bluetoothlespam.Helpers.StringHelpers.Companion.toHexString import de.simon.dankelmann.bluetoothlespam.PermissionCheck.PermissionCheck import de.simon.dankelmann.bluetoothlespam.databinding.ActivityMainBinding -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import java.lang.Exception +import org.w3c.dom.Text class MainActivity : AppCompatActivity() { @@ -63,7 +56,7 @@ class MainActivity : AppCompatActivity() { super.onCreate(savedInstanceState) // Initialize AppContext, Activity, Advertisement Service and QueHandler - AppContext.setContext(this) + AppContext.setContext(applicationContext) AppContext.setActivity(this) binding = ActivityMainBinding.inflate(layoutInflater) @@ -76,46 +69,51 @@ class MainActivity : AppCompatActivity() { // Listen to Preference changes var prefs = PreferenceManager.getDefaultSharedPreferences(this); - sharedPreferenceChangedListener = OnSharedPreferenceChangeListener { sharedPreferences, key -> - run { - var legacyAdvertisingKey = AppContext.getActivity().resources.getString(R.string.preference_key_use_legacy_advertising) - if (key == legacyAdvertisingKey) { - val advertisementService = BluetoothHelpers.getAdvertisementService() - AppContext.setAdvertisementService(advertisementService) - AppContext.getAdvertisementSetQueueHandler().setAdvertisementService(advertisementService) - } + sharedPreferenceChangedListener = + OnSharedPreferenceChangeListener { sharedPreferences, key -> + run { + var legacyAdvertisingKey = + AppContext.getActivity().resources.getString(R.string.preference_key_use_legacy_advertising) + if (key == legacyAdvertisingKey) { + val advertisementService = BluetoothHelpers.getAdvertisementService() + AppContext.setAdvertisementService(advertisementService) + AppContext.getAdvertisementSetQueueHandler() + .setAdvertisementService(advertisementService) + } - var intervalKey = AppContext.getActivity().resources.getString(R.string.preference_key_interval_advertising_queue_handler) - if (key == intervalKey) { - var newInterval = QueueHandlerHelpers.getInterval() - Log.d(_logTag, "Setting new Interval: $newInterval") - AppContext.getAdvertisementSetQueueHandler().setInterval(newInterval) + var intervalKey = + AppContext.getActivity().resources.getString(R.string.preference_key_interval_advertising_queue_handler) + if (key == intervalKey) { + var newInterval = QueueHandlerHelpers.getInterval() + Log.d(_logTag, "Setting new Interval: $newInterval") + AppContext.getAdvertisementSetQueueHandler().setInterval(newInterval) + } } } - } prefs.registerOnSharedPreferenceChangeListener(sharedPreferenceChangedListener); val drawerLayout: DrawerLayout = binding.drawerLayout val navView: NavigationView = binding.navView val navController = findNavController(R.id.nav_host_fragment_content_main) + + navView.getHeaderView(0).findViewById(R.id.textViewGithubLink) + ?.setOnClickListener { + val uri = Uri.parse("https://github.com/simondankelmann/Bluetooth-LE-Spam") + val intent = Intent(Intent.ACTION_VIEW, uri) + startActivity(intent) + } + // Passing each menu ID as a set of Ids because each // menu should be considered as top level destinations. appBarConfiguration = AppBarConfiguration( setOf( R.id.nav_start, - /* - R.id.nav_fast_pairing, - R.id.nav_swift_pair, - R.id.nav_continuity_action_modals, - R.id.nav_continuity_device_popups, - R.id.nav_kitchen_sink*/ ), drawerLayout ) setupActionBarWithNavController(navController, appBarConfiguration) navView.setupWithNavController(navController) - } private val bluetoothAdapter: BluetoothAdapter by lazy { diff --git a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/AdvertiseData.kt b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/AdvertiseData.kt index 196084fb..0b90448a 100644 --- a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/AdvertiseData.kt +++ b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/AdvertiseData.kt @@ -2,8 +2,9 @@ package de.simon.dankelmann.bluetoothlespam.Models import android.bluetooth.le.AdvertiseData import android.util.Log +import java.io.Serializable -class AdvertiseData { +class AdvertiseData : Serializable { private var _logTag = "AdvertiseData" var id = 0 diff --git a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/AdvertiseSettings.kt b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/AdvertiseSettings.kt index fc9092b8..89f4c1b1 100644 --- a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/AdvertiseSettings.kt +++ b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/AdvertiseSettings.kt @@ -5,8 +5,9 @@ import android.util.Log import androidx.room.ColumnInfo import de.simon.dankelmann.bluetoothlespam.Enums.AdvertiseMode import de.simon.dankelmann.bluetoothlespam.Enums.TxPowerLevel +import java.io.Serializable -class AdvertiseSettings { +class AdvertiseSettings : Serializable { private var _logTag = "AdvertiseSettingsModel" var id:Int = 0 var advertiseMode = AdvertiseMode.ADVERTISEMODE_LOW_LATENCY diff --git a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/AdvertisementSet.kt b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/AdvertisementSet.kt index cd6d6684..c98db871 100644 --- a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/AdvertisementSet.kt +++ b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/AdvertisementSet.kt @@ -7,8 +7,9 @@ import de.simon.dankelmann.bluetoothlespam.Enums.AdvertisementSetRange import de.simon.dankelmann.bluetoothlespam.Enums.AdvertisementSetType import de.simon.dankelmann.bluetoothlespam.Enums.AdvertisementState import de.simon.dankelmann.bluetoothlespam.Enums.AdvertisementTarget +import java.io.Serializable -class AdvertisementSet { +class AdvertisementSet : Serializable { private val _logTag = "AdvertisementSet" // Data diff --git a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/AdvertisementSetList.kt b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/AdvertisementSetList.kt index 0085dd18..4bedd3ea 100644 --- a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/AdvertisementSetList.kt +++ b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/AdvertisementSetList.kt @@ -1,6 +1,8 @@ package de.simon.dankelmann.bluetoothlespam.Models -class AdvertisementSetList { +import java.io.Serializable + +class AdvertisementSetList : Serializable { var title = "" var advertisementSets:MutableList = mutableListOf() diff --git a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/AdvertisingSetParameters.kt b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/AdvertisingSetParameters.kt index 1630794a..402f7b9e 100644 --- a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/AdvertisingSetParameters.kt +++ b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/AdvertisingSetParameters.kt @@ -8,8 +8,9 @@ import androidx.room.ColumnInfo import de.simon.dankelmann.bluetoothlespam.Enums.PrimaryPhy import de.simon.dankelmann.bluetoothlespam.Enums.SecondaryPhy import de.simon.dankelmann.bluetoothlespam.Enums.TxPowerLevel +import java.io.Serializable -class AdvertisingSetParameters { +class AdvertisingSetParameters : Serializable { private var _logTag = "AdvertisingSetParametersModel" var id:Int = 0 @@ -24,8 +25,6 @@ class AdvertisingSetParameters { var connectable = false var anonymous = false - - fun validate():Boolean{ //@Todo: implement validation here return true diff --git a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/LogEntryModel.kt b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/LogEntryModel.kt index 95853720..73c8b8d4 100644 --- a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/LogEntryModel.kt +++ b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/LogEntryModel.kt @@ -1,8 +1,9 @@ package de.simon.dankelmann.bluetoothlespam.Models import de.simon.dankelmann.bluetoothlespam.Constants.LogLevel +import java.io.Serializable -class LogEntryModel { +class LogEntryModel : Serializable { var message:String = "" var level: LogLevel = LogLevel.Info } \ No newline at end of file diff --git a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/ManufacturerSpecificData.kt b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/ManufacturerSpecificData.kt index e17b5255..deba7129 100644 --- a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/ManufacturerSpecificData.kt +++ b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/ManufacturerSpecificData.kt @@ -1,6 +1,8 @@ package de.simon.dankelmann.bluetoothlespam.Models -class ManufacturerSpecificData { +import java.io.Serializable + +class ManufacturerSpecificData : Serializable { var id = 0 var manufacturerId = 0 var manufacturerSpecificData = byteArrayOf() diff --git a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/PeriodicAdvertisingParameters.kt b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/PeriodicAdvertisingParameters.kt index 1cce1a60..99a332f5 100644 --- a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/PeriodicAdvertisingParameters.kt +++ b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/PeriodicAdvertisingParameters.kt @@ -1,8 +1,9 @@ package de.simon.dankelmann.bluetoothlespam.Models import androidx.room.ColumnInfo +import java.io.Serializable -class PeriodicAdvertisingParameters { +class PeriodicAdvertisingParameters : Serializable { var id = 0 var includeTxPowerLevel = false var interval = 0 diff --git a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/ServiceData.kt b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/ServiceData.kt index 0990a1d4..5093ff59 100644 --- a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/ServiceData.kt +++ b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Models/ServiceData.kt @@ -1,8 +1,9 @@ package de.simon.dankelmann.bluetoothlespam.Models import android.os.ParcelUuid +import java.io.Serializable -class ServiceData { +class ServiceData : Serializable { var id = 0 var serviceUuid:ParcelUuid? = null var serviceData:ByteArray? = null diff --git a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Services/AdvertisementForegroundService.kt b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Services/AdvertisementForegroundService.kt new file mode 100644 index 00000000..716def1b --- /dev/null +++ b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Services/AdvertisementForegroundService.kt @@ -0,0 +1,343 @@ +package de.simon.dankelmann.bluetoothlespam.Services + +import android.app.Notification +import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.graphics.Color +import android.os.Binder +import android.os.Build +import android.os.IBinder +import android.widget.RemoteViews +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat +import androidx.navigation.NavDeepLinkBuilder +import de.simon.dankelmann.bluetoothlespam.AppContext.AppContext +import de.simon.dankelmann.bluetoothlespam.Enums.AdvertisementError +import de.simon.dankelmann.bluetoothlespam.Enums.AdvertisementSetType +import de.simon.dankelmann.bluetoothlespam.Enums.AdvertisementState +import de.simon.dankelmann.bluetoothlespam.Enums.AdvertisementTarget +import de.simon.dankelmann.bluetoothlespam.Interfaces.Callbacks.IAdvertisementServiceCallback +import de.simon.dankelmann.bluetoothlespam.Interfaces.Callbacks.IAdvertisementSetQueueHandlerCallback +import de.simon.dankelmann.bluetoothlespam.MainActivity +import de.simon.dankelmann.bluetoothlespam.Models.AdvertisementSet +import de.simon.dankelmann.bluetoothlespam.R + +class AdvertisementForegroundService: IAdvertisementServiceCallback, IAdvertisementSetQueueHandlerCallback, Service() { + + private val _logTag = "AdvertisementForegroundService" + private val _channelId = "AdvertisementForegroundService" + private val _channelName = "Advertisement Foreground Service" + private val _channelDescription = "Advertisement Foreground Service Description" + private var _currentAdvertisementSet:AdvertisementSet? = null + private val _binder: IBinder = LocalBinder() + + companion object { + fun startService(context: Context, message: String) { + val startIntent = Intent(context, AdvertisementForegroundService::class.java) + startIntent.putExtra("inputExtra", message) + ContextCompat.startForegroundService(context, startIntent) + } + fun stopService(context: Context) { + val stopIntent = Intent(context, AdvertisementForegroundService::class.java) + context.stopService(stopIntent) + } + } + + override fun onCreate() { + super.onCreate() + + createNotificationChannel() + + startForeground(1, createNotification(null)) + + // Setup Callbacks + AppContext.getAdvertisementService().addAdvertisementServiceCallback(this) + AppContext.getAdvertisementSetQueueHandler().addAdvertisementQueueHandlerCallback(this) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + return START_STICKY + } + + override fun onBind(intent: Intent): IBinder? { + return _binder + } + + inner class LocalBinder : Binder() { + val service: AdvertisementForegroundService + get() = this@AdvertisementForegroundService + } + + override fun onDestroy() { + AppContext.getAdvertisementService().removeAdvertisementServiceCallback(this) + AppContext.getAdvertisementSetQueueHandler().removeAdvertisementQueueHandlerCallback(this) + super.onDestroy() + } + + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val notificationManager = AppContext.getActivity().getSystemService(NOTIFICATION_SERVICE) as NotificationManager + val mChannel = NotificationChannel(_channelId, _channelName, NotificationManager.IMPORTANCE_HIGH) + mChannel.description = _channelDescription + mChannel.enableLights(true) + mChannel.lightColor = Color.BLUE + notificationManager.createNotificationChannel(mChannel) + } + } + + private fun createNotification(advertisementSet: AdvertisementSet?): Notification { + + val pendingIntentTargeted = NavDeepLinkBuilder(this) + .setComponentName(MainActivity::class.java) + .setGraph(R.navigation.mobile_navigation) + .setDestination(R.id.nav_advertisement) + //.setArguments(bundle) + .createPendingIntent() + + + // Custom Layout + val notificationView = RemoteViews(packageName, R.layout.advertisement_foreground_service_notification) + + var title = "" + var subtitle = "" + var targetImageId = R.drawable.bluetooth + + if (advertisementSet != null) { + title = advertisementSet.title + subtitle = when (advertisementSet.type) { + AdvertisementSetType.ADVERTISEMENT_TYPE_UNDEFINED -> "Undefined Type" + + AdvertisementSetType.ADVERTISEMENT_TYPE_EASY_SETUP_BUDS -> "Easy Setup Buds" + AdvertisementSetType.ADVERTISEMENT_TYPE_EASY_SETUP_WATCH -> "Easy Setup Watch" + + AdvertisementSetType.ADVERTISEMENT_TYPE_FAST_PAIRING_DEVICE -> "Fast Pairing" + AdvertisementSetType.ADVERTISEMENT_TYPE_FAST_PAIRING_NON_PRODUCTION -> "Fast Pairing" + AdvertisementSetType.ADVERTISEMENT_TYPE_FAST_PAIRING_PHONE_SETUP -> "Fast Pairing" + AdvertisementSetType.ADVERTISEMENT_TYPE_FAST_PAIRING_DEBUG -> "Fast Pairing" + + AdvertisementSetType.ADVERTISEMENT_TYPE_CONTINUITY_DEVICE_POPUPS -> "Apple Device" + AdvertisementSetType.ADVERTISEMENT_TYPE_CONTINUITY_ACTION_MODALS -> "Apple Action" + + AdvertisementSetType.ADVERTISEMENT_TYPE_SWIFT_PAIRING -> "Swift Pairing" + } + + targetImageId = when (advertisementSet.target) { + AdvertisementTarget.ADVERTISEMENT_TARGET_SAMSUNG -> R.drawable.samsung + AdvertisementTarget.ADVERTISEMENT_TARGET_ANDROID -> R.drawable.ic_android + AdvertisementTarget.ADVERTISEMENT_TARGET_IOS -> R.drawable.apple + AdvertisementTarget.ADVERTISEMENT_TARGET_UNDEFINED -> R.drawable.bluetooth + AdvertisementTarget.ADVERTISEMENT_TARGET_WINDOWS -> R.drawable.microsoft + AdvertisementTarget.ADVERTISEMENT_TARGET_KITCHEN_SINK -> R.drawable.shuffle + } + } + + val toggleImageSrc = when(AppContext.getAdvertisementSetQueueHandler().isActive()){ + true -> R.drawable.pause + false -> R.drawable.play_arrow + } + + // Views for Custom Layout + notificationView.setTextViewText( + R.id.advertisementForegroundServiceNotificationTitleTextView, + title + ) + notificationView.setTextViewText( + R.id.advertisementForegroundServiceNotificationSubTitleTextView, + subtitle + ) + notificationView.setImageViewResource( + R.id.advertisementForegroundServiceNotificationTargetImageView, + targetImageId + ) + + notificationView.setImageViewResource( + R.id.advertisementForegroundServiceNotificationToggleImageView, + toggleImageSrc + ) + + val targetIconColor = + resources.getColor(R.color.tint_target_icon, AppContext.getContext().theme) + val buttonActiveColor = + resources.getColor(R.color.tint_button_active, AppContext.getContext().theme) + val buttonInActiveColor = + resources.getColor(R.color.tint_button_inactive, AppContext.getContext().theme) + + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + notificationView.setColorInt( + R.id.advertisementForegroundServiceNotificationTargetImageView, + "setColorFilter", + targetIconColor, + targetIconColor + ) + + notificationView.setColorInt( + R.id.advertisementForegroundServiceNotificationToggleImageView, + "setColorFilter", + buttonActiveColor, + buttonActiveColor + ) + + notificationView.setColorInt( + R.id.advertisementForegroundServiceNotificationStopImageView, + "setColorFilter", + buttonActiveColor, + buttonActiveColor + ) + } else { + notificationView.setInt( + R.id.advertisementForegroundServiceNotificationTargetImageView, + "setColorFilter", + targetIconColor + ) + + notificationView.setInt( + R.id.advertisementForegroundServiceNotificationStopImageView, + "setColorFilter", + buttonActiveColor + ) + + notificationView.setInt( + R.id.advertisementForegroundServiceNotificationToggleImageView, + "setColorFilter", + buttonActiveColor + ) + } + + if (advertisementSet != null) { + var titleColor = when (advertisementSet.advertisementState) { + AdvertisementState.ADVERTISEMENT_STATE_UNDEFINED -> resources.getColor( + R.color.color_title, + AppContext.getContext().theme + ) + + AdvertisementState.ADVERTISEMENT_STATE_STARTED -> resources.getColor( + R.color.color_title, + AppContext.getContext().theme + ) + + AdvertisementState.ADVERTISEMENT_STATE_SUCCEEDED -> resources.getColor( + R.color.color_title, + AppContext.getContext().theme + ) + + AdvertisementState.ADVERTISEMENT_STATE_FAILED -> resources.getColor( + R.color.log_error, + AppContext.getContext().theme + ) + } + + notificationView.setTextColor( + R.id.advertisementForegroundServiceNotificationTitleTextView, + titleColor + ) + } + + // Listeners for Custom Layout + val toggleIntent = Intent(AppContext.getActivity(), ToggleButtonListener::class.java) + val pendingToggleSwitchIntent = PendingIntent.getBroadcast( + AppContext.getActivity(), + 0, + toggleIntent, + PendingIntent.FLAG_MUTABLE + ) + + notificationView.setOnClickPendingIntent( + R.id.advertisementForegroundServiceNotificationToggleImageView, + pendingToggleSwitchIntent + ) + + val stopIntent = Intent(AppContext.getActivity(), StopButtonListener::class.java) + val pendingStopSwitchIntent = PendingIntent.getBroadcast( + AppContext.getActivity(), + 0, + stopIntent, + PendingIntent.FLAG_MUTABLE + ) + + notificationView.setOnClickPendingIntent( + R.id.advertisementForegroundServiceNotificationStopImageView, + pendingStopSwitchIntent + ) + + var contentText = "Bluetooth LE Spam" + if (advertisementSet != null) { + contentText = advertisementSet.title + } + + return NotificationCompat.Builder(AppContext.getActivity(), _channelId) + .setContentTitle("Bluetooth LE Spam") + .setContentText(contentText) + .setSmallIcon(R.drawable.bluetooth) + .setContentIntent(pendingIntentTargeted) + //.setColor(resources.getColor(R.color.blue_normal, AppContext.getContext().theme)) + //.setColorized(true) + .setStyle(NotificationCompat.DecoratedCustomViewStyle()) + .setChannelId(_channelId) + .setOngoing(true) + .setOnlyAlertOnce(true) + .setCustomBigContentView(notificationView) + .setCustomContentView(notificationView) + .setForegroundServiceBehavior(FOREGROUND_SERVICE_IMMEDIATE).build() + } + + private fun updateNotification(advertisementSet: AdvertisementSet?){ + val notification = createNotification(advertisementSet) + val notificationManager = AppContext.getActivity().getSystemService(NOTIFICATION_SERVICE) as NotificationManager + notificationManager.notify(1, notification) + } + + // Button Handlers + class ToggleButtonListener : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if(AppContext.getAdvertisementSetQueueHandler().isActive()){ + AppContext.getAdvertisementSetQueueHandler().deactivate() + } else { + AppContext.getAdvertisementSetQueueHandler().activate() + } + } + } + + class StopButtonListener : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + AppContext.getAdvertisementSetQueueHandler().deactivate() + stopService(AppContext.getActivity()) + } + } + + // Advertisement Service Callbacks + override fun onAdvertisementSetStart(advertisementSet: AdvertisementSet?) { + _currentAdvertisementSet = advertisementSet + updateNotification(advertisementSet) + } + + override fun onAdvertisementSetStop(advertisementSet: AdvertisementSet?) { + _currentAdvertisementSet = advertisementSet + updateNotification(advertisementSet) + } + + override fun onAdvertisementSetSucceeded(advertisementSet: AdvertisementSet?) { + _currentAdvertisementSet = advertisementSet + updateNotification(advertisementSet) + } + + override fun onAdvertisementSetFailed(advertisementSet: AdvertisementSet?, advertisementError: AdvertisementError) { + _currentAdvertisementSet = advertisementSet + updateNotification(advertisementSet) + } + + override fun onQueueHandlerActivated() { + updateNotification(_currentAdvertisementSet) + } + + override fun onQueueHandlerDeactivated() { + updateNotification(_currentAdvertisementSet) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Services/AdvertisementLoopService.kt b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Services/AdvertisementLoopService.kt deleted file mode 100644 index 8a692d60..00000000 --- a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Services/AdvertisementLoopService.kt +++ /dev/null @@ -1,185 +0,0 @@ -package de.simon.dankelmann.bluetoothlespam.Services - -import android.bluetooth.le.AdvertiseCallback -import android.bluetooth.le.AdvertiseSettings -import android.bluetooth.le.AdvertisingSet -import android.bluetooth.le.AdvertisingSetCallback -import android.os.CountDownTimer -import android.util.Log -import de.simon.dankelmann.bluetoothlespam.Interfaces.Callbacks.IBleAdvertisementServiceCallback -import de.simon.dankelmann.bluetoothlespam.Models.AdvertisementSet - -class AdvertisementLoopService (bluetoothLeAdvertisementService:BluetoothLeAdvertisementService) { - private var _logTag = "AdvertisementLoopService" - var advertising = false - private var _bluetoothLeAdvertisementService:BluetoothLeAdvertisementService = bluetoothLeAdvertisementService - - private var _advertisementSetCollections:MutableList> = mutableListOf() - private var _bleAdvertisementServiceCallback:MutableList = mutableListOf() - - private val _maxAdvertisers = 1 - private var _currentAdvertisers:MutableList = mutableListOf() - - private var countdownInterval = 1000 - private var millisInFuture = 10000 - private var timer:CountDownTimer = getTimer() - - fun setIntervalSeconds(interval:Int){ - timer.cancel() - - countdownInterval = interval * 1000 - millisInFuture = countdownInterval * 10 - - timer = getTimer() - if(advertising){ - timer.start() - } - } - - private fun getTimer():CountDownTimer{ - return object: CountDownTimer(millisInFuture.toLong(), countdownInterval.toLong()) { - override fun onTick(millisUntilFinished: Long) { - advertiseNextPackage(true) - //batchIt() - } - override fun onFinish() { - Log.d(_logTag, "Timer finished, restarting") - this.start() - } - } - } - - fun addAdvertisementSetCollection(advertisementSetCollection: List){ - var newCollection:MutableList = mutableListOf() - - advertisementSetCollection.map{ - var advertisementSetToAdd = it - // overwrite with own callbacks - advertisementSetToAdd.advertisingCallback = advertiseCallback - advertisementSetToAdd.advertisingSetCallback = advertisingSetCallback - newCollection.add(advertisementSetToAdd) - } - - _advertisementSetCollections.add(newCollection) - } - - fun startAdvertising(){ - val hardwareCheck = _bluetoothLeAdvertisementService.checkHardware() - Log.d(_logTag, "Hardware Check returns: ${hardwareCheck}"); - advertising = true - - timer.cancel() - timer = getTimer() - timer.start() - - //advertiseNextPackage() - - _bleAdvertisementServiceCallback.map { - it.onAdvertisementStarted() - } - } - - fun stopAdvertising(){ - advertising = false - - timer.cancel() - - stopAllAdvertisers() - _bleAdvertisementServiceCallback.map { - it.onAdvertisementStopped() - } - } - - fun stopAllAdvertisers(){ - _advertisementSetCollections.map { collection -> - collection.map {advertisementSet -> - _bluetoothLeAdvertisementService.stopAdvertiseSet(advertisementSet) - } - } - } - - fun cleanupAdvertisers(){ - if(_currentAdvertisers.count() >= _maxAdvertisers){ - // remove the first advertiser - var advertiserToRemove = _currentAdvertisers[0] - _bluetoothLeAdvertisementService.stopAdvertiseSet(advertiserToRemove) - _currentAdvertisers.removeAt(0) - Log.d(_logTag, "Removed advertiser for: " + advertiserToRemove.title) - } - } - - fun batchIt(){ - stopAllAdvertisers() - for (i in 0.._maxAdvertisers){ - advertiseNextPackage(false) - } - } - - - fun advertiseNextPackage(clean: Boolean = true){ - - if(advertising && _advertisementSetCollections.count() > 0){ - - // clean if there are already too many advertisers - if(clean){ - cleanupAdvertisers() - } - - val nextAdvertisementSetCollection = _advertisementSetCollections.random() - val nextAdvertisementSet = nextAdvertisementSetCollection.random() - - _currentAdvertisers.add(nextAdvertisementSet) - _bluetoothLeAdvertisementService.advertiseSet(nextAdvertisementSet) - - Log.d(_logTag, "Added advertiser for: " + nextAdvertisementSet.title); - } - } - - fun addBleAdvertisementServiceCallback(callback: IBleAdvertisementServiceCallback){ - _bleAdvertisementServiceCallback.add(callback) - } - - // Own Callbacks - val advertiseCallback:AdvertiseCallback = object : AdvertiseCallback() { - override fun onStartFailure(errorCode: Int) { - super.onStartFailure(errorCode) - _bleAdvertisementServiceCallback.map{ - it.onStartFailure(errorCode) - } - } - - override fun onStartSuccess(settingsInEffect: AdvertiseSettings?) { - super.onStartSuccess(settingsInEffect) - _bleAdvertisementServiceCallback.map{ - it.onStartSuccess(settingsInEffect) - } - } - } - - val advertisingSetCallback:AdvertisingSetCallback = object : AdvertisingSetCallback() { - override fun onAdvertisingSetStarted(advertisingSet: AdvertisingSet?, txPower: Int, status: Int) { - _bleAdvertisementServiceCallback.map{ - it.onAdvertisingSetStarted(advertisingSet, txPower, status) - } - } - - override fun onAdvertisingDataSet(advertisingSet: AdvertisingSet, status: Int) { - _bleAdvertisementServiceCallback.map{ - it.onAdvertisingDataSet(advertisingSet, status) - } - } - - override fun onScanResponseDataSet(advertisingSet: AdvertisingSet, status: Int) { - _bleAdvertisementServiceCallback.map{ - it.onScanResponseDataSet(advertisingSet, status) - } - } - - override fun onAdvertisingSetStopped(advertisingSet: AdvertisingSet) { - _bleAdvertisementServiceCallback.map{ - it.onAdvertisingSetStopped(advertisingSet) - } - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Services/AdvertisementSetQueHandler.kt b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Services/AdvertisementSetQueHandler.kt deleted file mode 100644 index 63c18fed..00000000 --- a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Services/AdvertisementSetQueHandler.kt +++ /dev/null @@ -1,166 +0,0 @@ -package de.simon.dankelmann.bluetoothlespam.Services - -import android.bluetooth.le.AdvertiseCallback -import android.bluetooth.le.AdvertiseSettings -import android.bluetooth.le.AdvertisingSet -import android.bluetooth.le.AdvertisingSetCallback -import android.os.Handler -import android.os.Looper -import android.util.Log -import de.simon.dankelmann.bluetoothlespam.Interfaces.Callbacks.IBleAdvertisementServiceCallback -import de.simon.dankelmann.bluetoothlespam.Models.AdvertisementSet - -class AdvertisementSetQueHandler(bluetoothLeAdvertisementService:BluetoothLeAdvertisementService) { - // private - private var _logTag = "AdvertisementSetQueHandler" - private var _currentAdvertiesementSet:AdvertisementSet? = null - private val _bluetoothLeAdvertisementService:BluetoothLeAdvertisementService = bluetoothLeAdvertisementService - private var _advertisementSetCollections:MutableList> = mutableListOf() - private var _bleAdvertisementServiceCallback:MutableList = mutableListOf() - private var _interval:Long = 1000 - - // public - var advertising = false - - fun addAdvertisementSetCollection(advertisementSetCollection: List){ - var newCollection:MutableList = mutableListOf() - - advertisementSetCollection.map{ - var advertisementSetToAdd = it - // overwrite with own callbacks - advertisementSetToAdd.advertisingCallback = advertiseCallback - advertisementSetToAdd.advertisingSetCallback = advertisingSetCallback - newCollection.add(advertisementSetToAdd) - } - - _advertisementSetCollections.add(newCollection) - } - - fun addBleAdvertisementServiceCallback(callback: IBleAdvertisementServiceCallback){ - _bleAdvertisementServiceCallback.add(callback) - } - - fun setIntervalSeconds(seconds:Int){ - _interval = (seconds * 1000).toLong() - } - - fun startAdvertising(){ - advertising = true - - advertiseNextAdvertisementSet() - - _bleAdvertisementServiceCallback.map { - it.onAdvertisementStarted() - } - } - - fun stopAdvertising(){ - advertising = false - - _bleAdvertisementServiceCallback.map { - it.onAdvertisementStopped() - } - } - - fun advertiseNextAdvertisementSet(){ - if(advertising && _advertisementSetCollections.isNotEmpty()){ - val nextAdvertisementSetCollection = _advertisementSetCollections.random() - val nextAdvertisementSet = nextAdvertisementSetCollection.random() - - _currentAdvertiesementSet = nextAdvertisementSet - _bluetoothLeAdvertisementService.advertiseSet(nextAdvertisementSet) - } - } - - fun advertisementSucceeded(){ - stopCurrentAdvertisementSet() - advertiseNextAdvertisementSet() - } - - fun advertisementFailed(){ - Log.d(_logTag, "Trying it again") - advertisementSucceeded() - } - - fun stopCurrentAdvertisementSet(){ - if(_currentAdvertiesementSet != null){ - _bluetoothLeAdvertisementService.stopAdvertiseSet(_currentAdvertiesementSet!!) - _currentAdvertiesementSet = null - } - } - - private fun runLocalCallback(success:Boolean){ - Handler(Looper.getMainLooper()).postDelayed(object : Runnable { - override fun run() { - if(success){ - advertisementSucceeded() - } else { - advertisementFailed() - } - } - }, _interval) - } - - // Callback implementation <--- MOVE TO BLE ADVERTISE SERVICE - // - -- --- ADVERTISECALLBACK --- -- - // - val advertiseCallback: AdvertiseCallback = object : AdvertiseCallback() { - override fun onStartFailure(errorCode: Int) { - super.onStartFailure(errorCode) - runLocalCallback(false) - _bleAdvertisementServiceCallback.map { - it.onStartFailure(errorCode) - } - } - - override fun onStartSuccess(settingsInEffect: AdvertiseSettings?) { - super.onStartSuccess(settingsInEffect) - runLocalCallback(true) - _bleAdvertisementServiceCallback.map { - it.onStartSuccess(settingsInEffect) - } - } - } - - // - -- --- ADVERTISINGSETCALLBACK BLUETOOTH 5 --- -- - // - val advertisingSetCallback: AdvertisingSetCallback = object : AdvertisingSetCallback() { - override fun onAdvertisingSetStarted(advertisingSet: AdvertisingSet?, txPower: Int, status: Int) { - if(status == AdvertisingSetCallback.ADVERTISE_SUCCESS){ - // STOP ADVERTISING DELAYED, WAIT FOR STOP-SUCCESS, THEN ADVERTISE THE NEXT - Handler(Looper.getMainLooper()).postDelayed(object : Runnable { - override fun run() { - stopCurrentAdvertisementSet() - } - }, _interval) - - } else{ - Log.d(_logTag, "Failed: ${status}") - runLocalCallback(false) - } - - _bleAdvertisementServiceCallback.map{ - it.onAdvertisingSetStarted(advertisingSet, txPower, status) - } - } - - override fun onAdvertisingDataSet(advertisingSet: AdvertisingSet, status: Int) { - _bleAdvertisementServiceCallback.map{ - it.onAdvertisingDataSet(advertisingSet, status) - } - } - - override fun onScanResponseDataSet(advertisingSet: AdvertisingSet, status: Int) { - _bleAdvertisementServiceCallback.map{ - it.onScanResponseDataSet(advertisingSet, status) - } - } - - override fun onAdvertisingSetStopped(advertisingSet: AdvertisingSet) { - _bleAdvertisementServiceCallback.map{ - it.onAdvertisingSetStopped(advertisingSet) - Log.d(_logTag, "ADVERTISING SET WAS STOPPED") - runLocalCallback(true) - } - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/ui/advertisement/AdvertisementFragment.kt b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/ui/advertisement/AdvertisementFragment.kt index caf1373a..1a028573 100644 --- a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/ui/advertisement/AdvertisementFragment.kt +++ b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/ui/advertisement/AdvertisementFragment.kt @@ -20,6 +20,8 @@ import de.simon.dankelmann.bluetoothlespam.Enums.AdvertisementSetType import de.simon.dankelmann.bluetoothlespam.Enums.AdvertisementState import de.simon.dankelmann.bluetoothlespam.Enums.AdvertisementTarget import de.simon.dankelmann.bluetoothlespam.Interfaces.Callbacks.IAdvertisementServiceCallback +import de.simon.dankelmann.bluetoothlespam.Interfaces.Callbacks.IAdvertisementSetQueueHandlerCallback +import de.simon.dankelmann.bluetoothlespam.MainActivity import de.simon.dankelmann.bluetoothlespam.Models.AdvertisementSet import de.simon.dankelmann.bluetoothlespam.Models.AdvertisementSetCollection import de.simon.dankelmann.bluetoothlespam.Models.AdvertisementSetList @@ -27,12 +29,11 @@ import de.simon.dankelmann.bluetoothlespam.R import de.simon.dankelmann.bluetoothlespam.databinding.FragmentAdvertisementBinding -class AdvertisementFragment : Fragment(), IAdvertisementServiceCallback { +class AdvertisementFragment : Fragment(), IAdvertisementServiceCallback, IAdvertisementSetQueueHandlerCallback { private val _logTag = "AdvertisementFragment" private var _viewModel: AdvertisementViewModel? = null private var _binding: FragmentAdvertisementBinding? = null - private var _advertisementSetCollection: AdvertisementSetCollection? = null private lateinit var _expandableListView:ExpandableListView private lateinit var _adapter: AdvertisementSetCollectionExpandableListViewAdapter @@ -44,35 +45,13 @@ class AdvertisementFragment : Fragment(), IAdvertisementServiceCallback { private lateinit var viewModel: AdvertisementViewModel override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + Log.d(_logTag, "onCreate") val viewModel = ViewModelProvider(this)[AdvertisementViewModel::class.java] _viewModel = viewModel _binding = FragmentAdvertisementBinding.inflate(inflater, container, false) val root: View = _binding!!.root _expandableListView = _binding!!.advertisementFragmentCollectionExpandableListview - - // Get AdvertisementSetCollection from Bundle - if(arguments != null){ - var advertisementSetCollectionArgumentKey = "advertisementSetCollection" - - var advertismentSetCollection = AdvertisementSetCollection() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - val type: Class = AdvertisementSetCollection::class.java - var collectionFromBundle = requireArguments().getSerializable(advertisementSetCollectionArgumentKey, type) - if(collectionFromBundle != null){ - advertismentSetCollection = collectionFromBundle - } - } else { - var collectionFromBundle = requireArguments().getSerializable(advertisementSetCollectionArgumentKey) - if(collectionFromBundle != null){ - advertismentSetCollection = collectionFromBundle as AdvertisementSetCollection - } - } - - setAdvertisementSetCollection(advertismentSetCollection) - _viewModel!!.advertisementQueueMode.postValue(AppContext.getAdvertisementSetQueueHandler().getAdvertisementQueueMode()) - } - setupUi() return root @@ -80,13 +59,29 @@ class AdvertisementFragment : Fragment(), IAdvertisementServiceCallback { override fun onResume() { super.onResume() + Log.d(_logTag, "onResume") AppContext.getAdvertisementSetQueueHandler().addAdvertisementServiceCallback(this) + AppContext.getAdvertisementSetQueueHandler().addAdvertisementQueueHandlerCallback(this) + syncWithQueueHandler() } override fun onPause() { + Log.d(_logTag, "onPause") super.onPause() AppContext.getAdvertisementSetQueueHandler().removeAdvertisementServiceCallback(this) - AppContext.getAdvertisementSetQueueHandler().deactivate() + AppContext.getAdvertisementSetQueueHandler().removeAdvertisementQueueHandlerCallback(this) + //AppContext.getAdvertisementSetQueueHandler().deactivate() + } + + override fun onDestroy() { + super.onDestroy() + //AppContext.getAdvertisementSetQueueHandler().deactivate(true) + } + + private fun syncWithQueueHandler(){ + setAdvertisementSetCollection(AppContext.getAdvertisementSetQueueHandler().getAdvertisementSetCollection()) + _viewModel!!.advertisementQueueMode.postValue(AppContext.getAdvertisementSetQueueHandler().getAdvertisementQueueMode()) + _viewModel!!.isAdvertising.postValue(AppContext.getAdvertisementSetQueueHandler().isActive()) } override fun onActivityCreated(savedInstanceState: Bundle?) { @@ -100,14 +95,12 @@ class AdvertisementFragment : Fragment(), IAdvertisementServiceCallback { AppContext.getAdvertisementSetQueueHandler().deactivate() _viewModel!!.isAdvertising.postValue(false) } else { - AppContext.getAdvertisementSetQueueHandler().activate() + AppContext.getAdvertisementSetQueueHandler().activate(true) _viewModel!!.isAdvertising.postValue(true) } } fun setAdvertisementSetCollection(advertisementSetCollection: AdvertisementSetCollection){ - _advertisementSetCollection = advertisementSetCollection - _viewModel!!.advertisementSetCollectionTitle.postValue(advertisementSetCollection.title) _viewModel!!.advertisementSetCollectionSubTitle.postValue(getAdvertisementSetCollectionSubTitle(advertisementSetCollection)) @@ -120,6 +113,7 @@ class AdvertisementFragment : Fragment(), IAdvertisementServiceCallback { private fun setupExpandableListView(advertisementSetCollection: AdvertisementSetCollection) { + Log.d(_logTag, "Collection: " + advertisementSetCollection.advertisementSetLists.count()) // Setup grouped Data var titleList = advertisementSetCollection.advertisementSetLists.toList() var dataList = HashMap>() @@ -332,4 +326,14 @@ class AdvertisementFragment : Fragment(), IAdvertisementServiceCallback { } } // END: AdvertismentServiceCallback + + override fun onQueueHandlerActivated() { + Log.d(_logTag, "onQueueHandlerActivated") + _viewModel!!.isAdvertising.postValue(true) + } + + override fun onQueueHandlerDeactivated() { + Log.d(_logTag, "onQueueHandlerDeactivated") + _viewModel!!.isAdvertising.postValue(false) + } } \ No newline at end of file diff --git a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/ui/preferences/PreferencesFragment.kt b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/ui/preferences/PreferencesFragment.kt index c7075274..aebbe9a2 100644 --- a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/ui/preferences/PreferencesFragment.kt +++ b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/ui/preferences/PreferencesFragment.kt @@ -1,11 +1,68 @@ package de.simon.dankelmann.bluetoothlespam.ui.preferences + import android.os.Bundle +import android.text.Spannable +import android.text.SpannableString +import android.text.style.ForegroundColorSpan +import android.util.Log +import android.view.ContextMenu +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.core.view.MenuHost +import androidx.core.view.MenuProvider +import androidx.lifecycle.Lifecycle +import androidx.navigation.findNavController +import androidx.navigation.ui.NavigationUI import androidx.preference.PreferenceFragmentCompat +import de.simon.dankelmann.bluetoothlespam.AppContext.AppContext +import de.simon.dankelmann.bluetoothlespam.MainActivity import de.simon.dankelmann.bluetoothlespam.R -class PreferencesFragment: PreferenceFragmentCompat() { + +class PreferencesFragment: PreferenceFragmentCompat(), MenuProvider { + private val _logTag = "PreferencesFragment" override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.preferences, rootKey) } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val menuHost: MenuHost = requireActivity() + + menuHost.addMenuProvider(this, viewLifecycleOwner) + } + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + //menuInflater.inflate(R.menu.main, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return false + } + + override fun onPrepareMenu(menu: Menu) { + super.onPrepareMenu(menu) + val menuItems = listOf(menu?.findItem(R.id.nav_preferences), menu?.findItem(R.id.nav_set_tx_power)) + + menuItems.forEach { menuItem -> + val actionSettingsMenuItem = menuItem + val title = actionSettingsMenuItem?.title.toString() + val spannable = SpannableString(title) + + var textColor = resources.getColor(R.color.text_color, AppContext.getContext().theme) + + if(menuItem?.itemId == R.id.nav_preferences) { + textColor = resources.getColor(R.color.text_color_light, AppContext.getContext().theme) + menuItem?.isEnabled = false + } + + spannable.setSpan(ForegroundColorSpan(textColor), 0, spannable.length, Spannable.SPAN_INCLUSIVE_INCLUSIVE) + actionSettingsMenuItem?.title = spannable + } + } } \ No newline at end of file diff --git a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/ui/start/StartFragment.kt b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/ui/start/StartFragment.kt index ee6f2a73..593aea2f 100644 --- a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/ui/start/StartFragment.kt +++ b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/ui/start/StartFragment.kt @@ -89,7 +89,6 @@ class StartFragment : Fragment() { checkAdvertisementService() checkDatabase() - return root } @@ -269,7 +268,7 @@ class StartFragment : Fragment() { // Bluetooth Support val textViewBluetoothSupport: TextView = binding.startFragmentTextViewBluetooth _viewModel!!.bluetoothSupport.observe(viewLifecycleOwner) { - textViewBluetoothSupport.text = "Bluetooth Version: $it" + textViewBluetoothSupport.text = "Bluetooth: $it" } // Missing Requirements Text @@ -497,9 +496,12 @@ class StartFragment : Fragment() { fun navigateToAdvertisementFragment(advertisementSetCollection: AdvertisementSetCollection){ AppContext.getActivity().runOnUiThread { - val bundle = bundleOf("advertisementSetCollection" to advertisementSetCollection) + //val bundle = bundleOf("advertisementSetCollection" to advertisementSetCollection) val navController = AppContext.getActivity().findNavController(R.id.nav_host_fragment_content_main) - navController.navigate(R.id.action_nav_start_to_nav_advertisement, bundle) + AppContext.getAdvertisementSetQueueHandler().deactivate() + AppContext.getAdvertisementSetQueueHandler().setAdvertisementSetCollection(advertisementSetCollection) + //navController.navigate(R.id.action_nav_start_to_nav_advertisement, bundle) + navController.navigate(R.id.action_nav_start_to_nav_advertisement) } } @@ -588,6 +590,7 @@ class StartFragment : Fragment() { Manifest.permission.BLUETOOTH_ADVERTISE, //Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT, + Manifest.permission.POST_NOTIFICATIONS ) var notGrantedPermissions:MutableList = mutableListOf() @@ -627,20 +630,24 @@ class StartFragment : Fragment() { fun checkAdvertisementService(){ var advertisementServiceIsReady = true - try { - val advertisementService = BluetoothHelpers.getAdvertisementService() - AppContext.setAdvertisementService(advertisementService) - } catch (e:Exception){ - addMissingRequirement("Advertisement Service not initialized") - advertisementServiceIsReady = false + if(!AppContext.advertisementServiceIsInitialized()){ + try { + val advertisementService = BluetoothHelpers.getAdvertisementService() + AppContext.setAdvertisementService(advertisementService) + } catch (e:Exception){ + addMissingRequirement("Advertisement Service not initialized") + advertisementServiceIsReady = false + } } - try { - var advertisementSetQueueHandler = AdvertisementSetQueueHandler() - AppContext.setAdvertisementSetQueueHandler(advertisementSetQueueHandler) - } catch (e:Exception){ - addMissingRequirement("Queue Handler not initialized") - advertisementServiceIsReady = false + if(!AppContext.advertisementSetQueueHandlerIsInitialized()){ + try { + var advertisementSetQueueHandler = AdvertisementSetQueueHandler() + AppContext.setAdvertisementSetQueueHandler(advertisementSetQueueHandler) + } catch (e:Exception){ + addMissingRequirement("Queue Handler not initialized") + advertisementServiceIsReady = false + } } _viewModel!!.advertisementServiceIsReady.postValue(advertisementServiceIsReady) diff --git a/app/src/main/res/drawable/exit.xml b/app/src/main/res/drawable/exit.xml new file mode 100644 index 00000000..a17ca78b --- /dev/null +++ b/app/src/main/res/drawable/exit.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/stop.xml b/app/src/main/res/drawable/stop.xml new file mode 100644 index 00000000..19bcbee7 --- /dev/null +++ b/app/src/main/res/drawable/stop.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/advertisement_foreground_service_notification.xml b/app/src/main/res/layout/advertisement_foreground_service_notification.xml new file mode 100644 index 00000000..eebdc0bc --- /dev/null +++ b/app/src/main/res/layout/advertisement_foreground_service_notification.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/nav_header_main.xml b/app/src/main/res/layout/nav_header_main.xml index 22f171b7..0e784177 100644 --- a/app/src/main/res/layout/nav_header_main.xml +++ b/app/src/main/res/layout/nav_header_main.xml @@ -28,7 +28,7 @@ android:textAppearance="@style/TextAppearance.AppCompat.Body1" /> diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 3586bb17..dfe10603 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -61,4 +61,11 @@ @color/grey_normal @color/blue_normal + + @color/text_color_light + @color/text_color + @color/text_color_light + @color/text_color + @color/text_color_light + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 2f0d2810..de74c5fe 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -65,4 +65,11 @@ @color/grey_normal @color/blue_normal + + @color/text_color_light + @color/text_color + @color/text_color_light + @color/text_color + @color/text_color_light + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 51484ef6..49cd3adf 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -1,8 +1,6 @@ - -