diff --git a/app/src/main/java/eu/darken/capod/common/UIConverter.kt b/app/src/main/java/eu/darken/capod/common/UIConverter.kt new file mode 100644 index 00000000..1d300385 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/common/UIConverter.kt @@ -0,0 +1,22 @@ +package eu.darken.capod.common + +import android.content.Context +import android.util.TypedValue + +object UIConverter { + fun convertDpToPixels(context: Context, dp: Float): Int { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + dp, + context.resources.displayMetrics + ).toInt() + } + + fun convertSpToPixels(context: Context, sp: Float): Int { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_SP, + sp, + context.resources.displayMetrics + ).toInt() + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/common/preferences/PercentSliderPreference.kt b/app/src/main/java/eu/darken/capod/common/preferences/PercentSliderPreference.kt new file mode 100644 index 00000000..da153a4e --- /dev/null +++ b/app/src/main/java/eu/darken/capod/common/preferences/PercentSliderPreference.kt @@ -0,0 +1,100 @@ +package eu.darken.capod.common.preferences + +import android.content.Context +import android.content.res.TypedArray +import android.os.Parcelable +import android.util.AttributeSet +import androidx.annotation.PluralsRes +import androidx.preference.DialogPreference +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import eu.darken.capod.R +import eu.darken.capod.common.preferences.PercentSliderPreferenceDialogFragment.Companion.newInstance +import kotlinx.parcelize.Parcelize + +class PercentSliderPreference(context: Context?, attrs: AttributeSet?) : DialogPreference(context, attrs) { + @get:PluralsRes val sliderTextPluralsResource: Int + + val min: Float + val max: Float + + private var internalValue = 0f + private var internalValueSet = false + + init { + val a = getContext().obtainStyledAttributes(attrs, R.styleable.PercentSliderPreference) + min = a.getFloat(R.styleable.PercentSliderPreference_pspMin, 0f) + max = a.getFloat(R.styleable.PercentSliderPreference_pspMax, 1f) + sliderTextPluralsResource = a.getResourceId(R.styleable.PercentSliderPreference_sliderText, 0) + a.recycle() + } + + // Always persist/notify the first time. + var value: Float + get() = internalValue + set(value) { + // Always persist/notify the first time. + val changed = internalValue != value + if (changed || !internalValueSet) { + internalValue = value + internalValueSet = true + persistFloat(value) + if (changed) { + notifyChanged() + } + } + } + + override fun onGetDefaultValue(a: TypedArray, index: Int): Int { + return a.getInteger(index, 0) + } + + override fun onSetInitialValue(restoreValue: Boolean, defaultValue: Any?) { + value = if (restoreValue) getPersistedFloat(internalValue) else defaultValue as Float + } + + override fun onSaveInstanceState(): Parcelable { + val superState = super.onSaveInstanceState() + if (isPersistent) { + // No need to save instance state since it's persistent + return superState + } + return SavedState( + value = value, + superState = superState, + ) + } + + override fun onRestoreInstanceState(state: Parcelable?) { + if (state?.javaClass != SavedState::class.java) { + // Didn't save state for us in onSaveInstanceState + return super.onRestoreInstanceState(state) + } + + val myState = state as SavedState + super.onRestoreInstanceState(myState.superState) + value = myState.value + } + + @Parcelize + data class SavedState( + val value: Float, + val superState: Parcelable, + ) : Parcelable + + companion object { + private const val DIALOG_FRAGMENT_TAG = "android.support.v7.preference.PreferenceFragment.DIALOG" + fun onDisplayPreferenceDialog(preferenceFragment: PreferenceFragmentCompat, preference: Preference): Boolean { + if (preference is PercentSliderPreference) { + val fragmentManager = preferenceFragment.fragmentManager + if (fragmentManager!!.findFragmentByTag(DIALOG_FRAGMENT_TAG) == null) { + val dialogFragment = newInstance(preference.getKey()) + dialogFragment.setTargetFragment(preferenceFragment, 0) + dialogFragment.show(fragmentManager, DIALOG_FRAGMENT_TAG) + } + return true + } + return false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/common/preferences/PercentSliderPreferenceDialogFragment.kt b/app/src/main/java/eu/darken/capod/common/preferences/PercentSliderPreferenceDialogFragment.kt new file mode 100644 index 00000000..5e184470 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/common/preferences/PercentSliderPreferenceDialogFragment.kt @@ -0,0 +1,91 @@ +package eu.darken.capod.common.preferences + +import android.os.Bundle +import android.view.Gravity +import android.widget.LinearLayout +import android.widget.LinearLayout.* +import android.widget.SeekBar +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.preference.PreferenceDialogFragmentCompat +import eu.darken.capod.common.UIConverter +import kotlin.math.roundToInt + +class PercentSliderPreferenceDialogFragment : PreferenceDialogFragmentCompat(), SeekBar.OnSeekBarChangeListener { + private val layoutContainer by lazy { LinearLayout(requireContext()) } + private val valueText by lazy { TextView(requireContext()) } + private val splashText by lazy { TextView(requireContext()) } + private val seekBar by lazy { SeekBar(requireContext()) } + + private val preferencePercent: PercentSliderPreference + get() = super.getPreference() as PercentSliderPreference + + override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) { + layoutContainer.apply { + orientation = VERTICAL + val px: Int = UIConverter.convertDpToPixels(requireContext(), 24f) + setPadding(px, 0, px, 0) + } + + splashText.apply { + if (preferencePercent.dialogMessage != null) splashText.text = preferencePercent.dialogMessage + layoutContainer.addView(this) + } + + valueText.apply { + gravity = Gravity.CENTER_HORIZONTAL + textSize = 32f + + layoutContainer.addView(this, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)) + } + + seekBar.apply { + setOnSeekBarChangeListener(this@PercentSliderPreferenceDialogFragment) + max = (preferencePercent.max * 100).roundToInt() + progress = (preferencePercent.value * 100).roundToInt() + layoutContainer.addView(this, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)) + } + + updateValueText() + builder.setView(layoutContainer) + builder.setNegativeButton(null, null) + super.onPrepareDialogBuilder(builder) + } + + override fun onDialogClosed(positiveResult: Boolean) { + if (!positiveResult) return + val value: Int = seekBar.progress + if (preferencePercent.callChangeListener(value)) { + preferencePercent.value = value / 100f + } + } + + override fun onProgressChanged(seek: SeekBar, value: Int, fromTouch: Boolean) { + if (value < preferencePercent.min) { + seek.progress = (preferencePercent.min * 100).roundToInt() + } + updateValueText() + } + + private fun updateValueText() { + val count: Int = seekBar.progress + valueText.text = if (preferencePercent.sliderTextPluralsResource != 0) { + resources.getQuantityString(preferencePercent.sliderTextPluralsResource, count, count) + } else { + "$count%" + } + } + + override fun onStartTrackingTouch(seekBar: SeekBar) {} + override fun onStopTrackingTouch(seekBar: SeekBar) {} + + companion object { + @JvmStatic fun newInstance(key: String): PercentSliderPreferenceDialogFragment { + val fragment = PercentSliderPreferenceDialogFragment() + val arguments = Bundle() + arguments.putString(ARG_KEY, key) + fragment.arguments = arguments + return fragment + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/main/core/GeneralSettings.kt b/app/src/main/java/eu/darken/capod/main/core/GeneralSettings.kt index e42ded83..9cb94096 100644 --- a/app/src/main/java/eu/darken/capod/main/core/GeneralSettings.kt +++ b/app/src/main/java/eu/darken/capod/main/core/GeneralSettings.kt @@ -48,12 +48,18 @@ class GeneralSettings @Inject constructor( false ) + val minimumSignalQuality = preferences.createFlowPreference( + "core.signal.minimum", + 0.25f + ) + override val preferenceDataStore: PreferenceDataStore = PreferenceStoreMapper( monitorMode, scannerMode, autoPause, autoPlay, showAll, + minimumSignalQuality, debugSettings.isAutoReportEnabled, ) } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/BasicSingleApplePodsCardVH.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/BasicSingleApplePodsCardVH.kt index 984a5493..761b4903 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/BasicSingleApplePodsCardVH.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/BasicSingleApplePodsCardVH.kt @@ -7,6 +7,7 @@ import eu.darken.capod.common.lists.binding import eu.darken.capod.databinding.OverviewPodsAppleSingleBasicItemBinding import eu.darken.capod.pods.core.apple.BasicSingleApplePods import eu.darken.capod.pods.core.getBatteryLevelHeadset +import eu.darken.capod.pods.core.getSignalQuality import eu.darken.capod.pods.core.lastSeenFormatted import java.time.Instant diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/DualApplePodsCardVH.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/DualApplePodsCardVH.kt index ee3bf5d7..72663684 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/DualApplePodsCardVH.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/DualApplePodsCardVH.kt @@ -4,14 +4,11 @@ import android.view.ViewGroup import eu.darken.capod.R import eu.darken.capod.common.lists.binding import eu.darken.capod.databinding.OverviewPodsAppleDualItemBinding +import eu.darken.capod.pods.core.* import eu.darken.capod.pods.core.HasDualPods.Pod import eu.darken.capod.pods.core.apple.DualApplePods import eu.darken.capod.pods.core.apple.DualApplePods.DeviceColor import eu.darken.capod.pods.core.apple.DualApplePods.LidState -import eu.darken.capod.pods.core.getBatteryLevelCase -import eu.darken.capod.pods.core.getBatteryLevelLeftPod -import eu.darken.capod.pods.core.getBatteryLevelRightPod -import eu.darken.capod.pods.core.lastSeenFormatted import java.time.Instant class DualApplePodsCardVH(parent: ViewGroup) : diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/SingleApplePodsCardVH.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/SingleApplePodsCardVH.kt index ab78a276..e7586563 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/SingleApplePodsCardVH.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/SingleApplePodsCardVH.kt @@ -7,6 +7,7 @@ import eu.darken.capod.common.lists.binding import eu.darken.capod.databinding.OverviewPodsAppleSingleItemBinding import eu.darken.capod.pods.core.apple.SingleApplePods import eu.darken.capod.pods.core.getBatteryLevelHeadset +import eu.darken.capod.pods.core.getSignalQuality import eu.darken.capod.pods.core.lastSeenFormatted import java.time.Instant diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/UnknownPodDeviceCardVH.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/UnknownPodDeviceCardVH.kt index 10382eb4..031c46ef 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/UnknownPodDeviceCardVH.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/UnknownPodDeviceCardVH.kt @@ -5,6 +5,7 @@ import eu.darken.capod.R import eu.darken.capod.common.lists.binding import eu.darken.capod.databinding.OverviewPodsUnknownItemBinding import eu.darken.capod.pods.core.PodDevice +import eu.darken.capod.pods.core.getSignalQuality import eu.darken.capod.pods.core.lastSeenFormatted import java.time.Instant diff --git a/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsFragment.kt b/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsFragment.kt index 999ad6cc..8a2e796d 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsFragment.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsFragment.kt @@ -3,8 +3,10 @@ package eu.darken.capod.main.ui.settings.general import androidx.annotation.Keep import androidx.fragment.app.viewModels import androidx.preference.ListPreference +import androidx.preference.Preference import dagger.hilt.android.AndroidEntryPoint import eu.darken.capod.R +import eu.darken.capod.common.preferences.PercentSliderPreference import eu.darken.capod.common.uix.PreferenceFragment2 import eu.darken.capod.main.core.GeneralSettings import eu.darken.capod.main.core.MonitorMode @@ -38,4 +40,10 @@ class GeneralSettingsFragment : PreferenceFragment2() { } super.onPreferencesCreated() } + + override fun onDisplayPreferenceDialog(preference: Preference) { + if (PercentSliderPreference.onDisplayPreferenceDialog(this, preference)) return + + super.onDisplayPreferenceDialog(preference) + } } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt b/app/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt index 704a890b..4c2f2e0b 100644 --- a/app/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt +++ b/app/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt @@ -93,7 +93,8 @@ class PodMonitor @Inject constructor( val mainDevice: Flow get() = devices.map { devices -> devices.maxByOrNull { it.rssi }?.let { - if (it.rssi > -85) it else null + val minimumSignalQuality = generalSettings.minimumSignalQuality.value + if (it.signalQuality > minimumSignalQuality) it else null } } diff --git a/app/src/main/java/eu/darken/capod/pods/core/PodDevice.kt b/app/src/main/java/eu/darken/capod/pods/core/PodDevice.kt index 63b119ef..e1b02657 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/PodDevice.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/PodDevice.kt @@ -19,16 +19,18 @@ interface PodDevice { val rssi: Int get() = scanResult.rssi + /** + * This is not correct but it works ¯\_(ツ)_/¯ + * The range of the RSSI is device specific (ROMs). + */ + val signalQuality: Float + get() = (100 - abs(rssi)) / 100f + val rawData: ByteArray val rawDataHex: String get() = rawData.joinToString(separator = " ") { String.format("%02X", it) } - fun getSignalQuality(context: Context): String { - val percentage = (100 - abs(rssi)) - return "~$percentage%" - } - fun getLabel(context: Context): String @get:DrawableRes diff --git a/app/src/main/java/eu/darken/capod/pods/core/PodDeviceExtensions.kt b/app/src/main/java/eu/darken/capod/pods/core/PodDeviceExtensions.kt index 07efb424..7a20595f 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/PodDeviceExtensions.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/PodDeviceExtensions.kt @@ -23,6 +23,11 @@ fun HasSinglePod.getBatteryLevelHeadset(context: Context): String = batteryHeadsetPercent?.let { "${(it * 100).roundToInt()}%" } ?: context.getString(R.string.general_value_not_available_label) +fun PodDevice.getSignalQuality(context: Context): String { + val percentage = 100 * signalQuality + return "~${percentage.roundToInt()}%" +} + private val lastSeenFormatter = RelativeDateTimeFormatter.getInstance() fun PodDevice.lastSeenFormatted(now: Instant): String { diff --git a/app/src/main/res/drawable/ic_baseline_signal_cellular_alt_24.xml b/app/src/main/res/drawable/ic_baseline_signal_cellular_alt_24.xml index f80d2d37..5a76c4b4 100644 --- a/app/src/main/res/drawable/ic_baseline_signal_cellular_alt_24.xml +++ b/app/src/main/res/drawable/ic_baseline_signal_cellular_alt_24.xml @@ -4,7 +4,7 @@ android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"> - + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml new file mode 100644 index 00000000..b8af393e --- /dev/null +++ b/app/src/main/res/values/attrs.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1243eea8..2f65a52a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -115,4 +115,6 @@ Charging In ear Microphone + Minimum signal quality + The minimum signal quality that a device needs to have to be considered yours. \ No newline at end of file diff --git a/app/src/main/res/xml/preferences_general.xml b/app/src/main/res/xml/preferences_general.xml index 516e4104..ca385f7b 100644 --- a/app/src/main/res/xml/preferences_general.xml +++ b/app/src/main/res/xml/preferences_general.xml @@ -1,5 +1,6 @@ - + + +