Refactor device profiles: Add creation UI and drag-to-reorder

This commit introduces a new UI for creating and editing device profiles. Users can now define profile names, select device models, set minimum signal quality, and optionally add identity and encryption keys.

The device manager screen now supports drag-and-drop reordering of profiles. The order of profiles in the list now determines their priority, with items at the top having higher priority. The internal data structure for device profiles has been updated, and the `PodMonitor` now sorts devices based on this new profile priority.
This commit is contained in:
darken
2025-09-29 13:37:00 +02:00
parent 679c9cd070
commit 98cf4ff3e7
24 changed files with 1189 additions and 91 deletions
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorOnSurface">
<path
android:fillColor="@android:color/white"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorOnSurface">
<path
android:fillColor="@android:color/white"
android:pathData="M20,9H4v2h16V9zM4,15h16v-2H4V15z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorOnSurface">
<path
android:fillColor="@android:color/white"
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
</vector>
@@ -53,13 +53,14 @@
</LinearLayout>
<ImageView
android:id="@+id/menu_button"
android:id="@+id/drag_handle"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="8dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="Menu"
android:src="@drawable/abc_ic_menu_overflow_material" />
android:contentDescription="@string/device_profiles_drag_handle_description"
android:src="@drawable/ic_baseline_drag_handle_24"
android:alpha="0.6" />
</LinearLayout>
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/card"
style="@style/MyCardView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:context=".main.ui.MainActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title_label"
style="@style/TextAppearance.Material3.TitleMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
android:text="@string/device_profiles_empty_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/description_label"
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="4dp"
android:text="@string/device_profiles_empty_description"
app:layout_constraintBottom_toTopOf="@id/add_profile_action"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title_label" />
<com.google.android.material.button.MaterialButton
android:id="@+id/add_profile_action"
style="@style/Widget.Material3.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/device_profiles_add_action"
app:icon="@drawable/ic_baseline_add_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/description_label" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
@@ -0,0 +1,316 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
style="@style/Widget.Material3.Toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:menu="@menu/device_profile_creation_menu"
app:navigationIcon="@drawable/ic_baseline_arrow_back_24"
app:title="@string/device_profiles_create_title"
app:titleTextAppearance="@style/TextAppearance.Material3.TitleLarge"
app:titleMarginStart="16dp" />
<androidx.core.widget.NestedScrollView
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Basic Profile Info Card -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardCornerRadius="12dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<com.google.android.material.textview.MaterialTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/device_profiles_basic_info_title"
android:textAppearance="?attr/textAppearanceSubtitle1"
android:textStyle="bold" />
<com.google.android.material.textview.MaterialTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="16dp"
android:text="@string/device_profiles_basic_info_description"
android:textAppearance="?attr/textAppearanceCaption"
android:textColor="?android:attr/textColorTertiary" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/name_input_layout"
style="@style/Widget.Material3.TextInputLayout.FilledBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:hint="@string/device_profiles_name_label">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/name_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/model_input_layout"
style="@style/Widget.Material3.TextInputLayout.FilledBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:hint="@string/device_profiles_model_label">
<AutoCompleteTextView
android:id="@+id/model_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:focusable="false"
android:cursorVisible="false" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/device_input_layout"
style="@style/Widget.Material3.TextInputLayout.FilledBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/device_profiles_paired_device_label">
<AutoCompleteTextView
android:id="@+id/device_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:focusable="false"
android:cursorVisible="false" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Signal Quality Card -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardCornerRadius="12dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<com.google.android.material.textview.MaterialTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/device_profiles_signal_quality_title"
android:textAppearance="?attr/textAppearanceSubtitle1"
android:textStyle="bold" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/signal_quality_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="20%"
android:textAppearance="?attr/textAppearanceBody2"
android:textColor="?android:attr/textColorSecondary" />
<com.google.android.material.textview.MaterialTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:text="@string/device_profiles_signal_quality_description"
android:textAppearance="?attr/textAppearanceCaption"
android:textColor="?android:attr/textColorTertiary" />
<com.google.android.material.slider.Slider
android:id="@+id/signal_quality_slider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:valueFrom="10"
android:valueTo="100"
android:stepSize="5"
android:value="20" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Identity Key Card -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:cardCornerRadius="12dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<com.google.android.material.textview.MaterialTextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/settings_maindevice_identitykey_label"
android:textAppearance="?attr/textAppearanceSubtitle1"
android:textStyle="bold" />
<com.google.android.material.button.MaterialButton
android:id="@+id/identity_key_guide_button"
style="@style/Widget.Material3.Button.TextButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/general_guide_action"
android:textSize="12sp"
app:icon="@drawable/ic_baseline_question_mark_24"
app:iconSize="16dp" />
</LinearLayout>
<com.google.android.material.textview.MaterialTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="12dp"
android:text="@string/settings_maindevice_identitykey_explanation"
android:textAppearance="?attr/textAppearanceCaption"
android:textColor="?android:attr/textColorTertiary" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/identity_key_input_layout"
style="@style/Widget.Material3.TextInputLayout.FilledBox"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/identity_key_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapCharacters"
android:maxLines="1"
android:fontFamily="monospace" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Encryption Key Card -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardCornerRadius="12dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<com.google.android.material.textview.MaterialTextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/settings_maindevice_encryptionkey_label"
android:textAppearance="?attr/textAppearanceSubtitle1"
android:textStyle="bold" />
<com.google.android.material.button.MaterialButton
android:id="@+id/encryption_key_guide_button"
style="@style/Widget.Material3.Button.TextButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/general_guide_action"
android:textSize="12sp"
app:icon="@drawable/ic_baseline_question_mark_24"
app:iconSize="16dp" />
</LinearLayout>
<com.google.android.material.textview.MaterialTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="12dp"
android:text="@string/settings_maindevice_encryptionkey_explanation"
android:textAppearance="?attr/textAppearanceCaption"
android:textColor="?android:attr/textColorTertiary" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/encryption_key_input_layout"
style="@style/Widget.Material3.TextInputLayout.FilledBox"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/encryption_key_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapCharacters"
android:maxLines="1"
android:fontFamily="monospace" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="16dp">
<ImageView
android:id="@+id/model_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp"
android:scaleType="centerInside"
tools:src="@drawable/devic_airpods_gen1_both" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/model_name"
android:layout_width="0dp"
android:layout_gravity="center_vertical"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="?attr/textAppearanceBody1"
android:textColor="?android:attr/textColorPrimary"
tools:text="AirPods Pro (Gen 2)" />
</LinearLayout>
@@ -40,10 +40,11 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/manage_devices_action"
style="@style/Widget.Material3.Button.OutlinedButton"
style="@style/Widget.Material3.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:icon="@drawable/ic_baseline_devices_other_24"
android:text="@string/general_manage_devices_action"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/device_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBody1"
android:textColor="?android:attr/textColorPrimary"
tools:text="My AirPods Pro" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/device_address"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textAppearance="?attr/textAppearanceCaption"
android:textColor="?android:attr/textColorSecondary"
tools:text="AA:BB:CC:DD:EE:FF" />
</LinearLayout>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_delete"
android:icon="@drawable/ic_baseline_delete_24"
android:title="@string/device_profiles_delete_action"
android:visible="false"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_save"
android:icon="@drawable/ic_baseline_save_24"
android:title="@string/device_profiles_save_action"
app:showAsAction="always" />
</menu>
+22 -1
View File
@@ -44,7 +44,28 @@
android:id="@+id/deviceManagerFragment"
android:name="eu.darken.capod.devices.ui.DeviceManagerFragment"
android:label="DeviceManagerFragment"
tools:layout="@layout/device_manager_fragment" />
tools:layout="@layout/device_manager_fragment">
<action
android:id="@+id/action_deviceManagerFragment_to_deviceProfileCreationFragment"
app:destination="@id/deviceProfileCreationFragment">
<argument
android:name="profileId"
app:argType="string"
app:nullable="true"
android:defaultValue="@null" />
</action>
</fragment>
<fragment
android:id="@+id/deviceProfileCreationFragment"
android:name="eu.darken.capod.devices.ui.DeviceProfileCreationFragment"
android:label="DeviceProfileCreationFragment"
tools:layout="@layout/device_profile_creation_fragment">
<argument
android:name="profileId"
app:argType="string"
app:nullable="true"
android:defaultValue="@null" />
</fragment>
<fragment
android:id="@+id/troubleShooterFragment"
android:name="eu.darken.capod.troubleshooter.ui.TroubleShooterFragment"
+38
View File
@@ -226,5 +226,43 @@
<string name="permission_post_notifications_label">Show notifications</string>
<string name="permission_post_notifications_description">"Allow CAPod to show notifications about your AirPods, e.g. their current status while connected."</string>
<!-- Device profiles -->
<string name="device_profiles_empty_title">No device profiles configured</string>
<string name="device_profiles_empty_description">Create device profiles to manage multiple devices with custom settings and priorities.</string>
<string name="device_profiles_add_action">Add profile</string>
<string name="device_profiles_create_title">Create Profile</string>
<string name="device_profiles_edit_title">Edit Profile</string>
<string name="device_profiles_name_label">Profile name</string>
<string name="device_profiles_name_hint">My AirPods Pro</string>
<string name="device_profiles_model_label">Device model</string>
<string name="device_profiles_model_hint">Select your device model</string>
<string name="device_profiles_paired_device_label">Paired device (optional)</string>
<string name="device_profiles_paired_device_hint">Select paired Bluetooth device</string>
<string name="device_profiles_priority_label">Priority</string>
<string name="device_profiles_priority_hint">0 (highest priority)</string>
<string name="device_profiles_priority_description">Lower numbers have higher priority. Use 0 for your main device.</string>
<string name="device_profiles_save_action">Save profile</string>
<string name="device_profiles_validation_name_required">Profile name is required</string>
<string name="device_profiles_validation_priority_invalid">Priority must be a number between 0 and 999</string>
<string name="device_profiles_priority_default">0</string>
<string name="device_profiles_identity_key_label">Identity Key: Not set</string>
<string name="device_profiles_identity_key_set">Configured</string>
<string name="device_profiles_encryption_key_label">Encryption Key: Not set</string>
<string name="device_profiles_encryption_key_set">Configured</string>
<string name="device_profiles_identity_key_title">Identity Key (IRK)</string>
<string name="device_profiles_identity_key_not_set">Not configured</string>
<string name="device_profiles_identity_key_description">Optional: Identity key for enhanced device recognition</string>
<string name="device_profiles_encryption_key_title">Proximity Encryption Key</string>
<string name="device_profiles_encryption_key_not_set">Not configured</string>
<string name="device_profiles_encryption_key_description">Optional: Encryption key for proximity data decryption</string>
<string name="device_profiles_drag_handle_description">Drag to reorder</string>
<string name="device_profiles_delete_title">Delete Profile</string>
<string name="device_profiles_delete_message">Are you sure you want to delete this profile? This action cannot be undone.</string>
<string name="device_profiles_delete_action">Delete</string>
<string name="device_profiles_basic_info_title">Device Information</string>
<string name="device_profiles_basic_info_description">Configure your device name, model, and optional Bluetooth pairing.</string>
<string name="device_profiles_signal_quality_title">Minimum Signal Quality</string>
<string name="device_profiles_signal_quality_description">Only detect devices with signal strength above this threshold. Lower values increase detection range but may cause false positives.</string>
</resources>