Repository: simondankelmann/Bluetooth-LE-Spam
Pull request #362: Added Option To Unselect Lists and Items
Author: login: DebugBoard
Created: 2025-03-12T19:17:19Z | Merged: 2025-03-14T09:22:04Z



Diff:
diff --git a/.kotlin/errors/errors-1741731584117.log b/.kotlin/errors/errors-1741731584117.log
new file mode 100644
index 00000000..f459fde7
--- /dev/null
+++ b/.kotlin/errors/errors-1741731584117.log
@@ -0,0 +1,45 @@
+kotlin version: 2.1.0
+error message: Daemon compilation failed: Unknown or invalid session 3
+java.lang.Exception: Unknown or invalid session 3
+	at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:69)
+	at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:65)
+	at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:240)
+	at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:159)
+	at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:111)
+	at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:76)
+	at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:63)
+	at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66)
+	at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62)
+	at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100)
+	at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62)
+	at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44)
+	at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41)
+	at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)
+	at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
+	at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
+	at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
+	at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
+	at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
+	at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
+	at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41)
+	at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59)
+	at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:174)
+	at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
+	at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:194)
+	at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:127)
+	at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:169)
+	at org.gradle.internal.Factories$1.create(Factories.java:31)
+	at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:263)
+	at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127)
+	at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:132)
+	at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164)
+	at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:133)
+	at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
+	at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
+	at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
+	at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48)
+	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
+	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
+	at java.base/java.lang.Thread.run(Unknown Source)
+
+
diff --git a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Adapters/AdvertisementSetCollectionExpandableListViewAdapter.kt b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Adapters/AdvertisementSetCollectionExpandableListViewAdapter.kt
index ad3b1770..54915989 100644
--- a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Adapters/AdvertisementSetCollectionExpandableListViewAdapter.kt
+++ b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Adapters/AdvertisementSetCollectionExpandableListViewAdapter.kt
@@ -47,6 +47,7 @@ class AdvertisementSetCollectionExpandableListViewAdapter internal constructor(
             convertView = itemBinding.root
             holder = ItemViewHolder()
             holder.label = itemBinding.listItemAdvertisementSetTextView
+            holder.checkbox = itemBinding.checkboxAdvertisementSet
             convertView.tag = holder
         } else {
             holder = convertView.tag as ItemViewHolder
@@ -71,6 +72,18 @@ class AdvertisementSetCollectionExpandableListViewAdapter internal constructor(
 
         holder.label?.text = advertisementSet.title
         holder.label?.setTextColor(textColor)
+        
+        // Set checkbox state from the model
+        holder.checkbox?.isChecked = advertisementSet.isChecked
+        
+        // Add listener to update the model when checkbox state changes
+        holder.checkbox?.setOnClickListener { view ->
+            val isChecked = (view as android.widget.CheckBox).isChecked
+            advertisementSet.isChecked = isChecked
+            // Prevent the click from propagating to the parent view
+            view.isPressed = false
+        }
+        
         return convertView
     }
 
@@ -104,6 +117,7 @@ class AdvertisementSetCollectionExpandableListViewAdapter internal constructor(
             convertView = groupBinding.root
             holder = GroupViewHolder()
             holder.label = groupBinding.listItemAdvertisementSetList
+            holder.checkbox = groupBinding.checkboxAdvertisementList
             convertView.tag = holder
         } else {
             holder = convertView.tag as GroupViewHolder
@@ -118,6 +132,26 @@ class AdvertisementSetCollectionExpandableListViewAdapter internal constructor(
 
         holder.label?.text = advertisementSetList.title
         holder.label?.setTextColor(textColor)
+        
+        // Set initial checkbox state based on children's state
+        val allChildrenChecked = dataList[advertisementSetList]?.all { it.isChecked } ?: false
+        holder.checkbox?.isChecked = allChildrenChecked
+        
+        // Handle checkbox click to update all children and prevent interference with group expansion
+        holder.checkbox?.setOnClickListener { view ->
+            val isChecked = (view as android.widget.CheckBox).isChecked
+            
+            // Update all children in this group
+            dataList[advertisementSetList]?.forEach { advertisementSet ->
+                advertisementSet.isChecked = isChecked
+            }
+            
+            // Notify data set changed to refresh all child views
+            notifyDataSetChanged()
+            
+            // Prevent the click from propagating to the parent view
+            view.isPressed = false
+        }
 
         return convertView
     }
@@ -132,10 +166,12 @@ class AdvertisementSetCollectionExpandableListViewAdapter internal constructor(
 
     inner class ItemViewHolder {
         internal var label: TextView? = null
+        internal var checkbox: android.widget.CheckBox? = null
     }
 
     inner class GroupViewHolder {
         internal var label: TextView? = null
+        internal var checkbox: android.widget.CheckBox? = null
     }
 }
 
@@ -229,10 +265,12 @@ class AdvertisementSetCollectionExpandableListViewAdapter  internal constructor(
 
     inner class ItemViewHolder {
         internal var label: TextView? = null
+        internal var checkbox: android.widget.CheckBox? = null
     }
 
     inner class GroupViewHolder {
         internal var label: TextView? = null
+        internal var checkbox: android.widget.CheckBox? = null
     }
 }
 */
diff --git a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Handlers/AdvertisementSetQueueHandler.kt b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Handlers/AdvertisementSetQueueHandler.kt
index 40d6115c..8c4dccba 100644
--- a/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Handlers/AdvertisementSetQueueHandler.kt
+++ b/app/src/main/java/de/simon/dankelmann/bluetoothlespam/Handlers/AdvertisementSetQueueHandler.kt
@@ -149,11 +149,30 @@ class AdvertisementSetQueueHandler(
         }
     }
 
+    fun hasCheckedItems(): Boolean {
+        // Check if any advertisement set is checked
+        for (list in _advertisementSetCollection.advertisementSetLists) {
+            for (set in list.advertisementSets) {
+                if (set.isChecked) {
+                    return true
+                }
+            }
+        }
+        return false
+    }
+
     fun activate(context: Context) {
         if (_active) {
             return
         }
 
+        // Check if any items are selected before activating
+        if (!hasCheckedItems()) {
+            // Show toast message if no items are selected
+            android.widget.Toast.makeText(context, "No items selected", android.widget.Toast.LENGTH_SHORT).show()
+            return
+        }
+
         _active = true
         AdvertisementForegroundService.startService(context)
         _advertisementQueueHandlerCallbacks.forEach { it ->
@@ -195,8 +214,15 @@ class AdvertisementSetQueueHandler(
         }
 
         if (_active) {
-            val preparedSet = prepareAdvertisementSet(nextSet)
-            _advertisementService.startAdvertisement(preparedSet)
+            // Only advertise if the set is checked
+            if (nextSet.isChecked) {
+                val preparedSet = prepareAdvertisementSet(nextSet)
+                _advertisementService.startAdvertisement(preparedSet)
+            } else {
+                // If the set is not checked, immediately move to the next one
+                Log.d(_logTag, "Skipping unchecked advertisement set: ${nextSet.title}")
+                onAdvertisementSucceeded()
+            }
         }
     }
 
@@ -223,9 +249,19 @@ class AdvertisementSetQueueHandler(
                     if(_advertisementSetCollection.advertisementSetLists.isNotEmpty()){
                         val firstList = _advertisementSetCollection.advertisementSetLists.first()
                         if(firstList.advertisementSets.isNotEmpty()){
-                            nextAdvertisementSetListIndex = 0
-                            nextAdvertisementSetIndex = 0
-                            nextAdvertisementSet = firstList.advertisementSets.first()
+                            // Find the first checked advertisement set
+                            val firstCheckedIndex = firstList.advertisementSets.indexOfFirst { it.isChecked }
+                            if (firstCheckedIndex >= 0) {
+                                nextAdvertisementSetListIndex = 0
+                                nextAdvertisementSetIndex = firstCheckedIndex
+                                nextAdvertisementSet = firstList.advertisementSets[firstCheckedIndex]
+                            } else {
+                                // If no checked items, use the first one but don't advertise
+                                nextAdvertisementSetListIndex = 0
+                                nextAdvertisementSetIndex = 0
+                                nextAdvertisementSet = firstList.advertisementSets.first()
+                                nextAdvertisementSet.isChecked = false
+                            }
                         }
                     }
                 }
@@ -235,36 +271,76 @@ class AdvertisementSetQueueHandler(
                 // If no AdvertisementSet is selected, select the first set in the first list
                 if(_currentAdvertisementSet == null){
                     if(_advertisementSetCollection.advertisementSetLists.isNotEmpty()){
-                        val firstList = _advertisementSetCollection.advertisementSetLists.first()
-                        if(firstList.advertisementSets.isNotEmpty()){
-                            nextAdvertisementSetListIndex = 0
-                            nextAdvertisementSetIndex = 0
-                            nextAdvertisementSet = firstList.advertisementSets.first()
+                        // Find the first list with checked items
+                        for (listIndex in _advertisementSetCollection.advertisementSetLists.indices) {
+                            val list = _advertisementSetCollection.advertisementSetLists[listIndex]
+                            val firstCheckedIndex = list.advertisementSets.indexOfFirst { it.isChecked }
+                            if (firstCheckedIndex >= 0) {
+                                nextAdvertisementSetListIndex = listIndex
+                                nextAdvertisementSetIndex = firstCheckedIndex
+                                nextAdvertisementSet = list.advertisementSets[firstCheckedIndex]
+                                break
+                            }
+                        }
+                        
+                        // If no checked items found, use the first item but don't advertise
+                        if (nextAdvertisementSet == null && _advertisementSetCollection.advertisementSetLists.isNotEmpty()) {
+                            val firstList = _advertisementSetCollection.advertisementSetLists.first()
+                            if (firstList.advertisementSets.isNotEmpty()) {
+                                nextAdvertisementSetListIndex = 0
+                                nextAdvertisementSetIndex = 0
+                                nextAdvertisementSet = firstList.advertisementSets.first()
+                            }
                         }
                     }
                 } else {
                     var selectedList = _advertisementSetCollection.advertisementSetLists[_currentAdvertisementSetListIndex]
                     Log.d(_logTag, "List: ${selectedList.title}, SETS: ${selectedList.advertisementSets.count()}, CurrentIndex: ${_currentAdvertisementSetIndex}")
-                    if(_currentAdvertisementSetIndex >= (selectedList.advertisementSets.count() - 1)){
-                        // SET ADVERTISEMENT SET INDEX TO 0
-                        nextAdvertisementSetIndex = 0
-
-                        // SELECT NEXT LIST
-                        if(_currentAdvertisementSetListIndex >= (_advertisementSetCollection.advertisementSetLists.count() - 1)){
-                            nextAdvertisementSetListIndex = 0
-                        } else {
-                            nextAdvertisementSetListIndex++
+                    
+                    // Find the next checked item in the current list
+                    var foundNextChecked = false
+                    for (i in (_currentAdvertisementSetIndex + 1) until selectedList.advertisementSets.size) {
+                        if (selectedList.advertisementSets[i].isChecked) {
+                            nextAdvertisementSetIndex = i
+                            nextAdvertisementSet = selectedList.advertisementSets[i]
+                            foundNextChecked = true
+                            break
+                        }
+                    }
+                    
+                    // If we didn't find a checked item in the current list, move to the next list
+                    if (!foundNextChecked) {
+                        // Find the next list with checked items
+                        var nextListFound = false
+                        var startListIndex = _currentAdvertisementSetListIndex
+                        
+                        // Loop through lists starting from the next one
+                        for (listOffset in 1.._advertisementSetCollection.advertisementSetLists.size) {
+                            val listIndex = (startListIndex + listOffset) % _advertisementSetCollection.advertisementSetLists.size
+                            val list = _advertisementSetCollection.advertisementSetLists[listIndex]
+                            
+                            // Find the first checked item in this list
+                            val firstCheckedIndex = list.advertisementSets.indexOfFirst { it.isChecked }
+                            if (firstCheckedIndex >= 0) {
+                                nextAdvertisementSetListIndex = listIndex
+                                nextAdvertisementSetIndex = firstCheckedIndex
+                                nextAdvertisementSet = list.advertisementSets[firstCheckedIndex]
+                                nextListFound = true
+                                break
+                            }
+                        }
+                        
+                        // If no checked items found in any list, go back to the first list and first item
+                        if (!nextListFound) {
+                            // If we've gone through all lists and found no checked items, start over
+                            val firstList = _advertisementSetCollection.advertisementSetLists.first()
+                            if (firstList.advertisementSets.isNotEmpty()) {
+                                nextAdvertisementSetListIndex = 0
+                                nextAdvertisementSetIndex = 0
+                                nextAdvertisementSet = firstList.advertisementSets.first()
+                            }
                         }
-
-                        selectedList = _advertisementSetCollection.advertisementSetLists[nextAdvertisementSetListIndex]
-
-                        // SET THE ITEM
-                        nextAdvertisementSet = selectedList.advertisementSets[nextAdvertisementSetIndex]
-                    } else {
-                        nextAdvertisementSetIndex++
-                        nextAdvertisementSet = selectedList.advertisementSets[nextAdvertisementSetIndex]
                     }
-
                 }
             }
 
@@ -274,36 +350,82 @@ class AdvertisementSetQueueHandler(
                     if(_advertisementSetCollection.advertisementSetLists.isNotEmpty()){
                         val firstList = _advertisementSetCollection.advertisementSetLists.first()
                         if(firstList.advertisementSets.isNotEmpty()){
-                            nextAdvertisementSetListIndex = 0
-                            nextAdvertisementSetIndex = 0
-                            nextAdvertisementSet = firstList.advertisementSets.first()
+                            // Find the first checked advertisement set
+                            val firstCheckedIndex = firstList.advertisementSets.indexOfFirst { it.isChecked }
+                            if (firstCheckedIndex >= 0) {
+                                nextAdvertisementSetListIndex = 0
+                                nextAdvertisementSetIndex = firstCheckedIndex
+                                nextAdvertisementSet = firstList.advertisementSets[firstCheckedIndex]
+                            } else {
+                                // If no checked items, use the first one but don't advertise
+                                nextAdvertisementSetListIndex = 0
+                                nextAdvertisementSetIndex = 0
+                                nextAdvertisementSet = firstList.advertisementSets.first()
+                            }
                         }
                     }
                 } else {
                     var selectedList = _advertisementSetCollection.advertisementSetLists[_currentAdvertisementSetListIndex]
                     Log.d(_logTag, "List: ${selectedList.title}, SETS: ${selectedList.advertisementSets.count()}, CurrentIndex: ${_currentAdvertisementSetIndex}")
-                    if(_currentAdvertisementSetIndex >= (selectedList.advertisementSets.count() - 1)){
-                        // SET ADVERTISEMENT SET INDEX TO 0
-                        nextAdvertisementSetIndex = 0
-
-                        selectedList = _advertisementSetCollection.advertisementSetLists[nextAdvertisementSetListIndex]
-
-                        // SET THE ITEM
-                        nextAdvertisementSetListIndex = _currentAdvertisementSetListIndex
-                        nextAdvertisementSet = selectedList.advertisementSets[nextAdvertisementSetIndex]
-                    } else {
-                        nextAdvertisementSetIndex++
-                        nextAdvertisementSet = selectedList.advertisementSets[nextAdvertisementSetIndex]
+                    
+                    // Find the next checked item in the current list
+                    var foundNextChecked = false
+                    for (i in (_currentAdvertisementSetIndex + 1) until selectedList.advertisementSets.size) {
+                        if (selectedList.advertisementSets[i].isChecked) {
+                            nextAdvertisementSetIndex = i
+                            nextAdvertisementSet = selectedList.advertisementSets[i]
+                            foundNextChecked = true
+                            break
+                        }
+                    }
+                    
+                    // If we didn't find a checked item, loop back to the beginning of the same list
+                    if (!foundNextChecked) {
+                        // Loop through the current list from the beginning
+                        for (i in 0.._currentAdvertisementSetIndex) {
+                            if (selectedList.advertisementSets[i].isChecked) {
+                                nextAdvertisementSetIndex = i
+                                nextAdvertisementSet = selectedList.advertisementSets[i]
+                                foundNextChecked = true
+                                break
+                            }
+                        }
+                        
+                        // If still no checked items found, keep the current item but don't advertise
+                        if (!foundNextChecked) {
+                            nextAdvertisementSetIndex = 0
+                            nextAdvertisementSet = selectedList.advertisementSets[0]
+                        }
                     }
-
                 }
             }
 
             AdvertisementQueueMode.ADVERTISEMENT_QUEUE_MODE_RANDOM -> {
-                nextAdvertisementSetListIndex = Random.nextInt(_advertisementSetCollection.advertisementSetLists.size);
-                val nextAdvertisementSetList = _advertisementSetCollection.advertisementSetLists.get(nextAdvertisementSetListIndex)
-                nextAdvertisementSetIndex = Random.nextInt(nextAdvertisementSetList.advertisementSets.size)
-                nextAdvertisementSet = nextAdvertisementSetList.advertisementSets[nextAdvertisementSetIndex]
+                // Create a list of all checked advertisement sets across all lists
+                val checkedSets = mutableListOf<Triple<Int, Int, AdvertisementSet>>()
+                
+                _advertisementSetCollection.advertisementSetLists.forEachIndexed { listIndex, list ->
+                    list.advertisementSets.forEachIndexed { setIndex, set ->
+                        if (set.isChecked) {
+                            checkedSets.add(Triple(listIndex, setIndex, set))
+                        }
+                    }
+                }
+                
+                // If we have checked items, randomly select one of them
+                if (checkedSets.isNotEmpty()) {
+                    val randomIndex = Random.nextInt(checkedSets.size)
+                    val selected = checkedSets[randomIndex]
+                    nextAdvertisementSetListIndex = selected.first
+                    nextAdvertisementSetIndex = selected.second
+                    nextAdvertisementSet = selected.third
+                } else {
+                    // If no checked items, just pick a random one but don't advertise it
+                    nextAdvertisementSetListIndex = Random.nextInt(_advertisementSetCollection.advertisementSetLists.size)
+                    val nextAdvertisementSetList = _advertisementSetCollection.advertisementSetLists[nextAdvertisementSetListIndex]
+                    nextAdvertisementSetIndex = Random.nextInt(nextAdvertisementSetList.advertisementSets.size)
+                    nextAdvertisementSet = nextAdvertisementSetList.advertisementSets[nextAdvertisementSetIndex]
+                }
             }
         }
 
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 9a688f9a..55d4b3b6 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
@@ -36,6 +36,7 @@ class AdvertisementSet : Serializable {
     // Ui Data
     var currentlyAdvertising = false
     var advertisementState = AdvertisementState.ADVERTISEMENT_STATE_UNDEFINED
+    var isChecked = true // Default to true to maintain backward compatibility
 
     fun validate():Boolean{
         //@todo: implement checks here
diff --git a/app/src/main/res/layout/list_item_advertisement_list.xml b/app/src/main/res/layout/list_item_advertisement_list.xml
index 5e8891aa..cb67005c 100644
--- a/app/src/main/res/layout/list_item_advertisement_list.xml
+++ b/app/src/main/res/layout/list_item_advertisement_list.xml
@@ -3,18 +3,28 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="50dp"
-    android:orientation="vertical">
+    android:orientation="horizontal"
+    android:gravity="center_vertical">
+
+    <CheckBox
+        android:id="@+id/checkboxAdvertisementList"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="?android:attr/expandableListPreferredItemPaddingLeft"
+        android:checked="true"
+        android:focusable="false"
+        android:clickable="true" />
 
     <TextView
         android:id="@+id/listItemAdvertisementSetList"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:paddingBottom="10dp"
-        android:paddingStart="?android:attr/expandableListPreferredItemPaddingLeft"
         android:paddingTop="10dp"
+        android:paddingBottom="10dp"
+        android:paddingStart="8dp"
+        android:text="Headline"
         android:textColor="@color/text_color"
         android:textSize="@dimen/textSizeHeadline3"
-        android:textStyle="bold"
-        android:text="Headline"/>
+        android:textStyle="bold" />
 
 </LinearLayout>
diff --git a/app/src/main/res/layout/list_item_advertisement_list1.xml b/app/src/main/res/layout/list_item_advertisement_list1.xml
new file mode 100644
index 00000000..71bc7a80
--- /dev/null
+++ b/app/src/main/res/layout/list_item_advertisement_list1.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="50dp"
+    android:orientation="vertical">
+
+    <TextView
+        android:id="@+id/listItemAdvertisementSetList"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingStart="?android:attr/expandableListPreferredItemPaddingLeft"
+        android:paddingTop="10dp"
+        android:paddingBottom="10dp"
+        android:text="Headline"
+        android:textColor="@color/text_color"
+        android:textSize="@dimen/textSizeHeadline3"
+        android:textStyle="bold" />
+
+</LinearLayout>
diff --git a/app/src/main/res/layout/list_item_advertisement_set.xml b/app/src/main/res/layout/list_item_advertisement_set.xml
index 2afd5202..699b8613 100644
--- a/app/src/main/res/layout/list_item_advertisement_set.xml
+++ b/app/src/main/res/layout/list_item_advertisement_set.xml
@@ -3,15 +3,25 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="50dp"
-    android:orientation="vertical">
+    android:orientation="horizontal"
+    android:gravity="center_vertical">
+
+    <CheckBox
+        android:id="@+id/checkboxAdvertisementSet"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="?android:attr/expandableListPreferredChildPaddingLeft"
+        android:checked="true"
+        android:focusable="false"
+        android:clickable="true" />
 
     <TextView
         android:id="@+id/listItemAdvertisementSetTextView"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:paddingBottom="10dp"
-        android:paddingLeft="?android:attr/expandableListPreferredChildPaddingLeft"
         android:paddingTop="10dp"
+        android:paddingStart="8dp"
         android:textSize="@dimen/textSizeHeadline4"
         android:text="Item"/>
 
diff --git a/app/src/main/res/layout/list_item_advertisement_set1.xml b/app/src/main/res/layout/list_item_advertisement_set1.xml
new file mode 100644
index 00000000..2afd5202
--- /dev/null
+++ b/app/src/main/res/layout/list_item_advertisement_set1.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="50dp"
+    android:orientation="vertical">
+
+    <TextView
+        android:id="@+id/listItemAdvertisementSetTextView"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingBottom="10dp"
+        android:paddingLeft="?android:attr/expandableListPreferredChildPaddingLeft"
+        android:paddingTop="10dp"
+        android:textSize="@dimen/textSizeHeadline4"
+        android:text="Item"/>
+
+</LinearLayout>


Reviews:
user: login: simondankelmann
state: APPROVED
author_association: OWNER
submitted_at: 2025-03-14T09:21:58Z
user: login: thgoebel
body: The ability to select things makes sense, I had thought of something similar when thinking about UX improvements.

With having checkboxes, the following two queue modes become obsolete (if my understanding is correct):
- MODE SINGLE: Just deselect everything that you don’t want.
- MODE LIST: Just select only a single list, and deselect all other lists.

I might send a PR to simplify this, it would be nice to have simpler QueueHandler logic.

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-29T15:42:24Z
user: login: thgoebel
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-29T15:52:04Z
user: login: thgoebel
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-29T17:28:57Z

Review comments:
path: .kotlin/errors/errors-1741731584117.log
user: login: thgoebel
body: This log file seems to have been added by accident.
created_at: 2025-03-29T15:07:13Z
author_association: CONTRIBUTOR
path: app/src/main/res/layout/list_item_advertisement_list1.xml
user: login: thgoebel
body: This layout does not seem to be used anywhere? So can be deleted?
created_at: 2025-03-29T15:07:40Z
author_association: CONTRIBUTOR
path: app/src/main/res/layout/list_item_advertisement_set1.xml
user: login: thgoebel
body: This layout does not seem to be used anywhere either?
created_at: 2025-03-29T15:09:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -149,11 +149,30 @@ class AdvertisementSetQueueHandler(
         }
     }
 
+    fun hasCheckedItems(): Boolean {
+        // Check if any advertisement set is checked
+        for (list in _advertisementSetCollection.advertisementSetLists) {
+            for (set in list.advertisementSets) {
+                if (set.isChecked) {
+                    return true
+                }
+            }
+        }
+        return false
+    }
+
     fun activate(context: Context) {
         if (_active) {
             return
         }
 
+        // Check if any items are selected before activating
+        if (!hasCheckedItems()) {
+            // Show toast message if no items are selected
+            android.widget.Toast.makeText(context, "No items selected", android.widget.Toast.LENGTH_SHORT).show()
path: app/src/main/java/de/simon/dankelmann/bluetoothlespam/Handlers/AdvertisementSetQueueHandler.kt
user: login: thgoebel
body: Don’t hardcode strings, use the strings.xml so that they can be translated.
created_at: 2025-03-29T15:16:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -195,8 +214,15 @@ class AdvertisementSetQueueHandler(
         }
 
         if (_active) {
-            val preparedSet = prepareAdvertisementSet(nextSet)
-            _advertisementService.startAdvertisement(preparedSet)
+            // Only advertise if the set is checked
+            if (nextSet.isChecked) {
+                val preparedSet = prepareAdvertisementSet(nextSet)
+                _advertisementService.startAdvertisement(preparedSet)
+            } else {
+                // If the set is not checked, immediately move to the next one
path: app/src/main/java/de/simon/dankelmann/bluetoothlespam/Handlers/AdvertisementSetQueueHandler.kt
user: login: thgoebel
body: Imho it should not be the job of `fun advertiseNextAdvertisementSet` to handle skipping items. Just by the names, it makes more sense for `fun selectNextAdvertisementSet` to contain that skipping logic.
created_at: 2025-03-29T15:20:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -71,6 +72,18 @@ class AdvertisementSetCollectionExpandableListViewAdapter internal constructor(
 
         holder.label?.text = advertisementSet.title
         holder.label?.setTextColor(textColor)
+        
+        // Set checkbox state from the model
+        holder.checkbox?.isChecked = advertisementSet.isChecked
+        
+        // Add listener to update the model when checkbox state changes
+        holder.checkbox?.setOnClickListener { view ->
path: app/src/main/java/de/simon/dankelmann/bluetoothlespam/Adapters/AdvertisementSetCollectionExpandableListViewAdapter.kt
user: login: thgoebel
body: You can use `Checkbox.setOnCheckedChangeListener` to have simpler code.
created_at: 2025-03-29T15:52:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -71,6 +72,18 @@ class AdvertisementSetCollectionExpandableListViewAdapter internal constructor(
 
         holder.label?.text = advertisementSet.title
         holder.label?.setTextColor(textColor)
+        
+        // Set checkbox state from the model
+        holder.checkbox?.isChecked = advertisementSet.isChecked
+        
+        // Add listener to update the model when checkbox state changes
+        holder.checkbox?.setOnClickListener { view ->
path: app/src/main/java/de/simon/dankelmann/bluetoothlespam/Adapters/AdvertisementSetCollectionExpandableListViewAdapter.kt
user: login: thgoebel
body: Nevermind. I gave it a try in #366, and when using onCheckedChangeListener the UI jumps around a lot. On the surface, it seems as if things were being reordered, or like views are recycled and not properly initialised. Not sure why.

So let’s keep onClickListener for now.
created_at: 2025-03-29T17:28:57Z
author_association: CONTRIBUTOR

PR comments:
user: login: simondankelmann
created_at: 2025-03-14T09:22:21Z
body: Thank you :)
author_association: OWNER
user: login: DebugBoard
created_at: 2025-03-14T14:27:59Z
body: No worries :3
author_association: COLLABORATOR
user: login: DebugBoard
created_at: 2025-03-29T16:12:14Z
body: Much needed tx
author_association: COLLABORATOR