mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
369b0207c4 | ||
|
|
ce737b45b9 | ||
|
|
cde3afdcd9 | ||
|
|
07b3f4320e | ||
|
|
e1c71cf6b6 | ||
|
|
c233cd2445 | ||
|
|
8f609ca989 | ||
|
|
2de2369600 | ||
|
|
d5d2ae784c | ||
|
|
56bedb64c4 | ||
|
|
3b48ca90d3 | ||
|
|
272ecc006f | ||
|
|
ddacfa6841 | ||
|
|
c5b1c417a4 | ||
|
|
38bc3be5b7 | ||
|
|
57cd823b63 | ||
|
|
6cd799130e | ||
|
|
a1c58e0a32 | ||
|
|
9cd4cea63d | ||
|
|
1d5e13cc24 | ||
|
|
6d2c4ba8ca | ||
|
|
357baa12e6 | ||
|
|
909a9ba194 | ||
|
|
79f68c3a0d | ||
|
|
5eb97cc966 | ||
|
|
e4f405ec65 | ||
|
|
f392df05f9 | ||
|
|
0f8e441a92 | ||
|
|
edbc944724 | ||
|
|
65eb92dcb1 | ||
|
|
7df24f9c76 | ||
|
|
5d72d0116b | ||
|
|
91a16f55d9 | ||
|
|
9ae3516a83 | ||
|
|
f3c20caa13 | ||
|
|
680bcbad2a | ||
|
|
5f60f2b5eb | ||
|
|
7efe6619d2 | ||
|
|
1a3c2b3d23 | ||
|
|
7cccff8a71 | ||
|
|
4a3e5056b6 | ||
|
|
16a880a92d | ||
|
|
79db1e2412 | ||
|
|
b6e8d69eff | ||
|
|
76c1b65b89 | ||
|
|
d7902bc86e |
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 332 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 760 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 761 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 605 KiB |
@@ -0,0 +1,28 @@
|
||||
name: Android CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and test
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
cache: gradle
|
||||
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew assembleDebug
|
||||
- name: Run tests
|
||||
run: ./gradlew testGplayDebugUnitTest testFossDebugUnitTest
|
||||
@@ -0,0 +1,17 @@
|
||||
name: "Validate Gradle Wrapper"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
validation:
|
||||
name: "Validation"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: gradle/wrapper-validation-action@v1
|
||||
@@ -1,11 +1,23 @@
|
||||
<img src="https://github.com/d4rken/capod/raw/main/.assets/banner.png" width="400">
|
||||
|
||||
# Companion App for AirPods (CAPod)
|
||||
|
||||

|
||||
[](https://github.com/d4rken/capod/releases/latest)
|
||||
|
||||
A companion app that adds support for AirPod specific features to Android.
|
||||
A companion app that adds support for AirPod specific features to Android:
|
||||
|
||||
Supported models:
|
||||
* Battery level for pods & cases.
|
||||
* Charging status for pods & case.
|
||||
* Additional infos about connection, microphone & case.
|
||||
* Can receive and show all nearby devices.
|
||||
* Ear detection with automatic play/pause.
|
||||
* Automatically connect phone & airpods.
|
||||
* Show popup when case is opened.
|
||||
|
||||
CAPod is ad-free. Some additional features require an in-app purchase.
|
||||
|
||||
Currently supported models:
|
||||
|
||||
* AirPods Gen1
|
||||
* AirPods Gen2
|
||||
@@ -18,17 +30,17 @@ Supported models:
|
||||
* Beats X
|
||||
* Beats Flex
|
||||
|
||||
## Support the project
|
||||
|
||||
* Buy the CAPod Pro In-App purchase on [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod)
|
||||
* [Buy me a coffee](https://www.buymeacoffee.com/tydarken)
|
||||
* Help translate CAPod [on Crowdin](https://crowdin.com/project/airpod-companion)
|
||||
|
||||
## Download
|
||||
|
||||
* [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod)
|
||||
* [GitHub](https://github.com/d4rken/capod/releases/latest)
|
||||
|
||||
## Support the project
|
||||
|
||||
* Buy the CAPod Pro In-App purchase on [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod)
|
||||
* Help translate CAPod [on Crowdin](https://crowdin.com/project/capod)
|
||||
* [Buy me a coffee](https://www.buymeacoffee.com/tydarken)
|
||||
|
||||
## Get help
|
||||
|
||||
* [Github Issues](https://github.com/d4rken/capod/issues)
|
||||
@@ -36,6 +48,8 @@ Supported models:
|
||||
|
||||
## Screenshots
|
||||
|
||||
<img src="https://github.com/d4rken/capod/raw/main/.assets/screenshots/1.png" width="200"><img src="https://github.com/d4rken/capod/raw/main/.assets/screenshots/2.png" width="200"><img src="https://github.com/d4rken/capod/raw/main/.assets/screenshots/3.png" width="200"><img src="https://github.com/d4rken/capod/raw/main/.assets/screenshots/4.png" width="200">
|
||||
|
||||
## Thanks to
|
||||
|
||||
* The [OpenPods](https://github.com/adolfintel/OpenPods) project,
|
||||
@@ -49,8 +63,9 @@ Supported models:
|
||||
|
||||
CAPod's code is available under a GPL v3 license, this excludes:
|
||||
|
||||
* CAPod icons, logos, mascots and marketing materials.
|
||||
* CAPod icons, logos, mascots and marketing materials/assets.
|
||||
* CAPod animations and videos.
|
||||
* CAPod documentation.
|
||||
* Google Play screenshots.
|
||||
* Google Play texts & descriptions.
|
||||
* Translations.
|
||||
|
||||
+5
-5
@@ -14,10 +14,7 @@ import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.retryWhen
|
||||
import kotlinx.coroutines.flow.*
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.coroutines.resume
|
||||
@@ -112,8 +109,11 @@ class BillingClientConnectionProvider @Inject constructor(
|
||||
delay(3000 * attempt)
|
||||
true
|
||||
}
|
||||
.catch {
|
||||
log(TAG, ERROR) { "Unable to provide client connection:\n${it.asLog()}" }
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG: String = logTag("Upgrade", "Gplay", "Billing", "ClientProvider")
|
||||
val TAG: String = logTag("Upgrade", "Gplay", "Billing", "Client", "ConnectionProvider")
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,8 @@
|
||||
android:usesPermissionFlags="neverForLocation" />
|
||||
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.bluetooth_le"
|
||||
android:required="true" />
|
||||
|
||||
@@ -16,5 +16,6 @@ object BuildConfigWrap {
|
||||
val VERSION_NAME: String = BuildConfig.VERSION_NAME
|
||||
val GIT_SHA: String = BuildConfig.GITSHA
|
||||
|
||||
val VERSION_DESCRIPTION: String = "v$VERSION_NAME ($VERSION_CODE) [$GIT_SHA] ${FLAVOR}_$BUILD_TYPE"
|
||||
val VERSION_DESCRIPTION_LONG: String = "v$VERSION_NAME ($VERSION_CODE) [$GIT_SHA] ${FLAVOR}_$BUILD_TYPE"
|
||||
val VERSION_DESCRIPTION_SHORT: String = "v$VERSION_NAME [$GIT_SHA] $FLAVOR"
|
||||
}
|
||||
|
||||
@@ -5,10 +5,16 @@ import java.util.*
|
||||
fun Byte.toHex(): String = String.format("%02X", this)
|
||||
fun UByte.toHex(): String = this.toByte().toHex()
|
||||
|
||||
val Byte.upperNibble get() = (this.toInt() shr 4 and 0b1111).toByte()
|
||||
val Byte.lowerNibble get() = (this.toInt() and 0b1111).toByte()
|
||||
val UByte.upperNibble get() = (this.toInt() shr 4 and 0b1111).toUByte()
|
||||
val UByte.lowerNibble get() = (this.toInt() and 0b1111).toUByte()
|
||||
val Byte.upperNibble get() = (this.toInt() shr 4 and 0b1111).toShort()
|
||||
val Byte.lowerNibble get() = (this.toInt() and 0b1111).toShort()
|
||||
val UByte.upperNibble get() = (this.toInt() shr 4 and 0b1111).toUShort()
|
||||
val UByte.lowerNibble get() = (this.toInt() and 0b1111).toUShort()
|
||||
|
||||
fun Byte.isBitSet(pos: Int): Boolean = BitSet.valueOf(arrayOf(this).toByteArray()).get(pos)
|
||||
fun UByte.isBitSet(pos: Int): Boolean = this.toByte().isBitSet(pos)
|
||||
fun UByte.isBitSet(pos: Int): Boolean = this.toByte().isBitSet(pos)
|
||||
|
||||
fun Short.isBitSet(pos: Int): Boolean = this.toByte().isBitSet(pos)
|
||||
fun UShort.isBitSet(pos: Int): Boolean = this.toShort().isBitSet(pos)
|
||||
|
||||
fun UShort.toBinaryString(): String = Integer.toBinaryString(this.toInt()).padStart(4, '0')
|
||||
fun UByte.toBinaryString(): String = Integer.toBinaryString(this.toInt()).padStart(8, '0')
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package eu.darken.capod.common
|
||||
|
||||
object PrivacyPolicy {
|
||||
const val URL = "https://raw.githubusercontent.com/d4rken/capod-public/main/PRIVACY_POLICY.md"
|
||||
}
|
||||
@@ -17,19 +17,12 @@ import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
@Singleton
|
||||
class BluetoothManager2 @Inject constructor(
|
||||
@@ -68,90 +61,96 @@ class BluetoothManager2 @Inject constructor(
|
||||
awaitClose { context.unregisterReceiver(receiver) }
|
||||
}
|
||||
|
||||
suspend fun getBluetoothProfile(
|
||||
profile: Int = BluetoothProfile.HEADSET
|
||||
): BluetoothProfile2 = withContext(dispatcherProvider.IO) {
|
||||
fun getBluetoothProfile(profile: Int = BluetoothProfile.HEADSET): Flow<BluetoothProfile2> = callbackFlow {
|
||||
log(TAG, VERBOSE) { "getBluetoothProfile(profile=$profile)" }
|
||||
|
||||
suspendCancellableCoroutine {
|
||||
val connectionState = AtomicBoolean(false)
|
||||
manager.adapter.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
||||
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
||||
log(TAG, VERBOSE) { "onServiceConnected(profile=$profile, proxy=$proxy)" }
|
||||
connectionState.set(true)
|
||||
BluetoothProfile2(
|
||||
profileType = profile,
|
||||
profileProxy = proxy,
|
||||
isConnectedAtomic = connectionState
|
||||
).run { it.resume(this) }
|
||||
}
|
||||
var profileProxy: BluetoothProfile2? = null
|
||||
manager.adapter.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
||||
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
||||
log(TAG, VERBOSE) { "onServiceConnected(profile=$profile, proxy=$proxy)" }
|
||||
|
||||
override fun onServiceDisconnected(profile: Int) {
|
||||
log(TAG, WARN) { "onServiceDisconnected(profile=$profile" }
|
||||
connectionState.set(false)
|
||||
it.cancel(IOException("BluetoothProfile service disconnected (profile=$profile)"))
|
||||
}
|
||||
profileProxy = BluetoothProfile2(
|
||||
profileType = profile,
|
||||
profileProxy = proxy,
|
||||
).also { trySend(it) }
|
||||
}
|
||||
|
||||
}, profile)
|
||||
override fun onServiceDisconnected(profile: Int) {
|
||||
log(TAG, WARN) { "onServiceDisconnected(profile=$profile" }
|
||||
close(IOException("BluetoothProfile service disconnected (profile=$profile)"))
|
||||
}
|
||||
|
||||
}, profile)
|
||||
|
||||
awaitClose {
|
||||
log(TAG) { "Closing BluetoothProfile: $profileProxy" }
|
||||
profileProxy?.let {
|
||||
manager.adapter.closeProfileProxy(it.profileType, it.proxy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun connectedDevicesForProfile(profile: Int): Flow<Set<BluetoothDevice>> = callbackFlow {
|
||||
log(TAG, VERBOSE) { "connectedDevices(profile=$profile) starting" }
|
||||
trySend(getBluetoothProfile(profile).connectedDevices)
|
||||
|
||||
val filter = IntentFilter().apply {
|
||||
addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
|
||||
addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
|
||||
}
|
||||
private fun monitorDevicesForProfile(
|
||||
profile: Int = BluetoothProfile.HEADSET
|
||||
): Flow<Set<BluetoothDevice>> = getBluetoothProfile(profile).flatMapLatest { bluetoothProfile ->
|
||||
callbackFlow {
|
||||
log(TAG, VERBOSE) { "connectedDevices(profile=$profile) starting" }
|
||||
trySend(bluetoothProfile.connectedDevices)
|
||||
|
||||
val handlerThread = HandlerThread("BluetoothEventReceiver").apply {
|
||||
start()
|
||||
}
|
||||
val handler = Handler(handlerThread.looper)
|
||||
val filter = IntentFilter().apply {
|
||||
addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
|
||||
addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
|
||||
}
|
||||
|
||||
val receiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
log(TAG, VERBOSE) { "Bluetooth event (intent=$intent, extras=${intent.extras})" }
|
||||
val action = intent.action
|
||||
if (action == null) {
|
||||
log(TAG, ERROR) { "Bluetooth event without action, how did we get this?" }
|
||||
return
|
||||
}
|
||||
val device = intent.getParcelableExtra<BluetoothDevice?>(BluetoothDevice.EXTRA_DEVICE)
|
||||
if (device == null) {
|
||||
log(TAG, ERROR) { "Connection event is missing EXTRA_DEVICE: ${intent.extras}" }
|
||||
return
|
||||
}
|
||||
val handlerThread = HandlerThread("BluetoothEventReceiver").apply {
|
||||
start()
|
||||
}
|
||||
val handler = Handler(handlerThread.looper)
|
||||
|
||||
this@callbackFlow.launch {
|
||||
val currentDevices = getBluetoothProfile(profile).connectedDevices
|
||||
val receiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
log(TAG, VERBOSE) { "Bluetooth event (intent=$intent, extras=${intent.extras})" }
|
||||
val action = intent.action
|
||||
if (action == null) {
|
||||
log(TAG, ERROR) { "Bluetooth event without action, how did we get this?" }
|
||||
return
|
||||
}
|
||||
val device = intent.getParcelableExtra<BluetoothDevice?>(BluetoothDevice.EXTRA_DEVICE)
|
||||
if (device == null) {
|
||||
log(TAG, ERROR) { "Connection event is missing EXTRA_DEVICE: ${intent.extras}" }
|
||||
return
|
||||
}
|
||||
|
||||
when (action) {
|
||||
BluetoothDevice.ACTION_ACL_CONNECTED -> {
|
||||
log(TAG) { "Adding $device to current devices $currentDevices" }
|
||||
trySend(currentDevices.plus(device))
|
||||
}
|
||||
BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
|
||||
log(TAG) { "Removing $device from current devices $currentDevices" }
|
||||
trySend(currentDevices.minus(device))
|
||||
this@callbackFlow.launch {
|
||||
val currentDevices = bluetoothProfile.connectedDevices
|
||||
|
||||
when (action) {
|
||||
BluetoothDevice.ACTION_ACL_CONNECTED -> {
|
||||
log(TAG) { "Adding $device to current devices $currentDevices" }
|
||||
trySend(currentDevices.plus(device))
|
||||
}
|
||||
BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
|
||||
log(TAG) { "Removing $device from current devices $currentDevices" }
|
||||
trySend(currentDevices.minus(device))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
context.registerReceiver(receiver, filter, null, handler)
|
||||
context.registerReceiver(receiver, filter, null, handler)
|
||||
|
||||
awaitClose {
|
||||
log(TAG, VERBOSE) { "connectedDevices(profile=$profile) closed." }
|
||||
context.unregisterReceiver(receiver)
|
||||
awaitClose {
|
||||
log(TAG, VERBOSE) { "connectedDevices(profile=$profile) closed." }
|
||||
context.unregisterReceiver(receiver)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun connectedDevices(
|
||||
featureFilter: Set<ParcelUuid> = ContinuityProtocol.BLE_FEATURE_UUIDS
|
||||
): Flow<List<BluetoothDevice>> = isBluetoothEnabled
|
||||
.flatMapLatest { connectedDevicesForProfile(BluetoothProfile.HEADSET) }
|
||||
.flatMapLatest { monitorDevicesForProfile(BluetoothProfile.HEADSET) }
|
||||
.map { devices ->
|
||||
devices.filter { device ->
|
||||
featureFilter.any { feature ->
|
||||
@@ -162,21 +161,23 @@ class BluetoothManager2 @Inject constructor(
|
||||
|
||||
fun bondedDevices(): Set<BluetoothDevice> = adapter.bondedDevices
|
||||
|
||||
suspend fun nudgeConnection(device: BluetoothDevice): Boolean = try {
|
||||
log(TAG) { "Nudging Android connection to $device" }
|
||||
suspend fun nudgeConnection(device: BluetoothDevice): Boolean = getBluetoothProfile().map { bluetoothProfile ->
|
||||
try {
|
||||
log(TAG) { "Nudging Android connection to $device" }
|
||||
|
||||
val connectMethod = BluetoothHeadset::class.java.getDeclaredMethod(
|
||||
"connect", BluetoothDevice::class.java
|
||||
).apply { isAccessible = true }
|
||||
val connectMethod = BluetoothHeadset::class.java.getDeclaredMethod(
|
||||
"connect", BluetoothDevice::class.java
|
||||
).apply { isAccessible = true }
|
||||
|
||||
connectMethod.invoke(getBluetoothProfile().profile, device)
|
||||
connectMethod.invoke(bluetoothProfile.proxy, device)
|
||||
|
||||
log(TAG) { "Nudged connection to $device" }
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Bugs.report(tag = TAG, "BluetoothHeadset.connect(device) is unavailable", exception = e)
|
||||
false
|
||||
}
|
||||
log(TAG) { "Nudged connection to $device" }
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Bugs.report(tag = TAG, "BluetoothHeadset.connect(device) is unavailable", exception = e)
|
||||
false
|
||||
}
|
||||
}.first()
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("Bluetooth", "Manager2")
|
||||
|
||||
@@ -2,23 +2,15 @@ package eu.darken.capod.common.bluetooth
|
||||
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothProfile
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
data class BluetoothProfile2(
|
||||
private val profileType: Int,
|
||||
internal val profileType: Int,
|
||||
private val profileProxy: BluetoothProfile,
|
||||
private val isConnectedAtomic: AtomicBoolean,
|
||||
) {
|
||||
|
||||
val profile: BluetoothProfile
|
||||
get() {
|
||||
if (!isConnected) throw IllegalStateException("Proxy is not connected")
|
||||
return profileProxy
|
||||
}
|
||||
val proxy: BluetoothProfile
|
||||
get() = profileProxy
|
||||
|
||||
val connectedDevices: Set<BluetoothDevice>
|
||||
get() = profile.connectedDevices.toSet()
|
||||
|
||||
val isConnected: Boolean
|
||||
get() = isConnectedAtomic.get()
|
||||
get() = proxy.connectedDevices.toSet()
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ class RecorderModule @Inject constructor(
|
||||
context.startServiceCompat(Intent(context, RecorderService::class.java))
|
||||
|
||||
log(TAG, INFO) { "Build.Fingerprint: ${Build.FINGERPRINT}" }
|
||||
log(TAG, INFO) { "BuildConfig.Versions: ${BuildConfigWrap.VERSION_DESCRIPTION}" }
|
||||
log(TAG, INFO) { "BuildConfig.Versions: ${BuildConfigWrap.VERSION_DESCRIPTION_LONG}" }
|
||||
|
||||
copy(
|
||||
recorder = newRecorder
|
||||
|
||||
@@ -89,7 +89,7 @@ class RecorderActivityVM @Inject constructor(
|
||||
type = "application/zip"
|
||||
|
||||
addCategory(Intent.CATEGORY_DEFAULT)
|
||||
putExtra(Intent.EXTRA_SUBJECT, "CAPod DebugLog - ${BuildConfigWrap.VERSION_DESCRIPTION})")
|
||||
putExtra(Intent.EXTRA_SUBJECT, "CAPod DebugLog - ${BuildConfigWrap.VERSION_DESCRIPTION_LONG})")
|
||||
putExtra(Intent.EXTRA_TEXT, "Your text here.")
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
|
||||
@@ -62,6 +62,15 @@ enum class Permission(
|
||||
val pwm = it.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
pwm.isIgnoringBatteryOptimizations(BuildConfigWrap.APPLICATION_ID)
|
||||
},
|
||||
),
|
||||
SYSTEM_ALERT_WINDOW(
|
||||
minApiLevel = Build.VERSION_CODES.BASE,
|
||||
labelRes = R.string.permission_system_alert_window_label,
|
||||
descriptionRes = R.string.permission_system_alert_window_description,
|
||||
permissionId = "android.permission.SYSTEM_ALERT_WINDOW",
|
||||
isGranted = {
|
||||
android.provider.Settings.canDrawOverlays(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class GeneralSettings @Inject constructor(
|
||||
|
||||
val scannerMode = preferences.createFlowPreference(
|
||||
"core.scanner.mode",
|
||||
ScannerMode.BALANCED,
|
||||
ScannerMode.LOW_LATENCY,
|
||||
moshi
|
||||
)
|
||||
|
||||
|
||||
@@ -1,20 +1,48 @@
|
||||
package eu.darken.capod.main.core
|
||||
|
||||
import android.content.Context
|
||||
import dagger.Reusable
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.permissions.Permission
|
||||
import eu.darken.capod.common.permissions.isRequired
|
||||
import eu.darken.capod.reaction.settings.ReactionSettings
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Reusable
|
||||
@Singleton
|
||||
class PermissionTool @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val generalSettings: GeneralSettings,
|
||||
private val reactionSettings: ReactionSettings,
|
||||
) {
|
||||
private val permissionCheckTrigger = MutableStateFlow(UUID.randomUUID())
|
||||
|
||||
suspend fun missingPermissions(): Set<Permission> = Permission.values()
|
||||
.filter { it != Permission.IGNORE_BATTERY_OPTIMIZATION || generalSettings.monitorMode.value == MonitorMode.ALWAYS }
|
||||
.filter { it.isRequired(context) }
|
||||
.toSet()
|
||||
fun recheck() {
|
||||
log(TAG) { "recheck()" }
|
||||
permissionCheckTrigger.value = UUID.randomUUID()
|
||||
}
|
||||
|
||||
val missingPermissions: Flow<Set<Permission>> = combine(
|
||||
permissionCheckTrigger,
|
||||
generalSettings.monitorMode.flow,
|
||||
reactionSettings.showPopUpOnCaseOpen.flow
|
||||
) { _, monitorMode, showPopUp ->
|
||||
Permission.values()
|
||||
.filter { it != Permission.IGNORE_BATTERY_OPTIMIZATION || monitorMode == MonitorMode.ALWAYS }
|
||||
.filter { it != Permission.ACCESS_BACKGROUND_LOCATION || monitorMode == MonitorMode.ALWAYS }
|
||||
.filter { it != Permission.SYSTEM_ALERT_WINDOW || showPopUp }
|
||||
.filter { it.isRequired(context) }
|
||||
.toSet()
|
||||
}
|
||||
.onEach { log(TAG) { "Missing permission: $it" } }
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("PermissionTool")
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,8 @@ import eu.darken.capod.common.lists.differ.setupDiffer
|
||||
import eu.darken.capod.common.lists.modular.ModularAdapter
|
||||
import eu.darken.capod.common.lists.modular.mods.DataBinderMod
|
||||
import eu.darken.capod.common.lists.modular.mods.TypedVHCreatorMod
|
||||
import eu.darken.capod.main.ui.overview.cards.BluetoothDisabledVH
|
||||
import eu.darken.capod.main.ui.overview.cards.MissingMainDeviceVH
|
||||
import eu.darken.capod.main.ui.overview.cards.NoPairedDeviceCardVH
|
||||
import eu.darken.capod.main.ui.overview.cards.PermissionCardVH
|
||||
import eu.darken.capod.main.ui.overview.cards.pods.BasicSingleApplePodsCardVH
|
||||
import eu.darken.capod.main.ui.overview.cards.pods.DualApplePodsCardVH
|
||||
@@ -33,8 +33,8 @@ class OverviewAdapter @Inject constructor() :
|
||||
modules.add(TypedVHCreatorMod({ data[it] is SingleApplePodsCardVH.Item }) { SingleApplePodsCardVH(it) })
|
||||
modules.add(TypedVHCreatorMod({ data[it] is BasicSingleApplePodsCardVH.Item }) { BasicSingleApplePodsCardVH(it) })
|
||||
modules.add(TypedVHCreatorMod({ data[it] is UnknownPodDeviceCardVH.Item }) { UnknownPodDeviceCardVH(it) })
|
||||
modules.add(TypedVHCreatorMod({ data[it] is NoPairedDeviceCardVH.Item }) { NoPairedDeviceCardVH(it) })
|
||||
modules.add(TypedVHCreatorMod({ data[it] is MissingMainDeviceVH.Item }) { MissingMainDeviceVH(it) })
|
||||
modules.add(TypedVHCreatorMod({ data[it] is BluetoothDisabledVH.Item }) { BluetoothDisabledVH(it) })
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = data.size
|
||||
|
||||
@@ -33,11 +33,11 @@ class OverviewFragment : Fragment3(R.layout.main_fragment) {
|
||||
lateinit var adapter: OverviewAdapter
|
||||
|
||||
lateinit var permissionLauncher: ActivityResultLauncher<String>
|
||||
var awaitingIgnoreBatteryOptimization = false
|
||||
var awaitingPermission = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
awaitingIgnoreBatteryOptimization = savedInstanceState?.getBoolean("awaitingIgnoreBatteryOptimization") ?: false
|
||||
awaitingPermission = savedInstanceState?.getBoolean("awaitingPermission") ?: false
|
||||
|
||||
permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||
log { "Request for $id was granted=$granted" }
|
||||
@@ -70,21 +70,35 @@ class OverviewFragment : Fragment3(R.layout.main_fragment) {
|
||||
}
|
||||
}
|
||||
|
||||
vm.listItems.observe2(ui) {
|
||||
adapter.update(it)
|
||||
vm.listItems.observe2(ui) { adapter.update(it) }
|
||||
|
||||
vm.workerAutolaunch.observe2 {
|
||||
// While UI is active, subscribe to the autolaunch routine
|
||||
}
|
||||
|
||||
vm.requestPermissionEvent.observe2(ui) {
|
||||
if (it == Permission.IGNORE_BATTERY_OPTIMIZATION) {
|
||||
awaitingIgnoreBatteryOptimization = true
|
||||
startActivity(
|
||||
Intent(
|
||||
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
|
||||
Uri.parse("package:${requireContext().packageName}")
|
||||
when (it) {
|
||||
Permission.IGNORE_BATTERY_OPTIMIZATION -> {
|
||||
awaitingPermission = true
|
||||
startActivity(
|
||||
Intent(
|
||||
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
|
||||
Uri.parse("package:${requireContext().packageName}")
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
permissionLauncher.launch(it.permissionId)
|
||||
}
|
||||
Permission.SYSTEM_ALERT_WINDOW -> {
|
||||
awaitingPermission = true
|
||||
startActivity(
|
||||
Intent(
|
||||
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
||||
Uri.parse("package:${requireContext().packageName}")
|
||||
)
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
permissionLauncher.launch(it.permissionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,15 +147,15 @@ class OverviewFragment : Fragment3(R.layout.main_fragment) {
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putBoolean("awaitingIgnoreBatteryOptimization", awaitingIgnoreBatteryOptimization)
|
||||
outState.putBoolean("awaitingPermission", awaitingPermission)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (awaitingIgnoreBatteryOptimization) {
|
||||
awaitingIgnoreBatteryOptimization = false
|
||||
log { "awaitingIgnoreBatteryOptimization=true" }
|
||||
if (awaitingPermission) {
|
||||
awaitingPermission = false
|
||||
log { "awaitingPermission=true" }
|
||||
vm.onPermissionResult(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@ import android.app.Activity
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import eu.darken.capod.common.WebpageTool
|
||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.flow.combine
|
||||
import eu.darken.capod.common.livedata.SingleLiveEvent
|
||||
import eu.darken.capod.common.navigation.navVia
|
||||
import eu.darken.capod.common.permissions.Permission
|
||||
@@ -16,8 +18,8 @@ import eu.darken.capod.common.upgrade.UpgradeRepo
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
import eu.darken.capod.main.core.MonitorMode
|
||||
import eu.darken.capod.main.core.PermissionTool
|
||||
import eu.darken.capod.main.ui.overview.cards.BluetoothDisabledVH
|
||||
import eu.darken.capod.main.ui.overview.cards.MissingMainDeviceVH
|
||||
import eu.darken.capod.main.ui.overview.cards.NoPairedDeviceCardVH
|
||||
import eu.darken.capod.main.ui.overview.cards.PermissionCardVH
|
||||
import eu.darken.capod.main.ui.overview.cards.pods.*
|
||||
import eu.darken.capod.monitor.core.PodMonitor
|
||||
@@ -26,7 +28,6 @@ import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.isActive
|
||||
@@ -45,6 +46,7 @@ class OverviewFragmentVM @Inject constructor(
|
||||
debugSettings: DebugSettings,
|
||||
private val upgradeRepo: UpgradeRepo,
|
||||
private val bluetoothManager: BluetoothManager2,
|
||||
private val webpageTool: WebpageTool,
|
||||
) : ViewModel3(dispatcherProvider = dispatcherProvider) {
|
||||
|
||||
val upgradeState = upgradeRepo.upgradeInfo.asLiveData2()
|
||||
@@ -57,9 +59,7 @@ class OverviewFragmentVM @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private val permissionCheckTrigger = MutableStateFlow(UUID.randomUUID())
|
||||
private val requiredPermissions: Flow<Collection<Permission>> = permissionCheckTrigger
|
||||
.map { permissionTool.missingPermissions() }
|
||||
val workerAutolaunch: LiveData<Unit> = permissionTool.missingPermissions
|
||||
.onEach {
|
||||
if (it.isNotEmpty()) {
|
||||
log(TAG) { "Missing permissions: $it" }
|
||||
@@ -76,26 +76,24 @@ class OverviewFragmentVM @Inject constructor(
|
||||
monitorControl.startMonitor()
|
||||
}
|
||||
}
|
||||
.map { Unit }
|
||||
.asLiveData2()
|
||||
|
||||
val requestPermissionEvent = SingleLiveEvent<Permission>()
|
||||
|
||||
private val pods: Flow<List<PodDevice>> = requiredPermissions
|
||||
private val pods: Flow<List<PodDevice>> = permissionTool.missingPermissions
|
||||
.flatMapLatest { permissions ->
|
||||
if (permissions.isEmpty()) {
|
||||
generalSettings.showAll.flow
|
||||
.flatMapLatest { showAll ->
|
||||
if (showAll) {
|
||||
podMonitor.devices
|
||||
} else {
|
||||
podMonitor.mainDevice.map { mainDevice ->
|
||||
mainDevice?.let { listOf(it) } ?: emptyList()
|
||||
}
|
||||
}
|
||||
if (permissions.isNotEmpty()) {
|
||||
return@flatMapLatest flowOf(emptyList())
|
||||
}
|
||||
|
||||
generalSettings.showAll.flow.flatMapLatest { showAll ->
|
||||
if (showAll) {
|
||||
podMonitor.devices
|
||||
} else {
|
||||
podMonitor.mainDevice.map { mainDevice ->
|
||||
mainDevice?.let { listOf(it) } ?: emptyList()
|
||||
}
|
||||
} else {
|
||||
channelFlow {
|
||||
send(emptyList())
|
||||
awaitClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,17 +101,36 @@ class OverviewFragmentVM @Inject constructor(
|
||||
|
||||
val listItems: LiveData<List<OverviewAdapter.Item>> = combine(
|
||||
updateTicker,
|
||||
requiredPermissions,
|
||||
permissionTool.missingPermissions,
|
||||
pods,
|
||||
debugSettings.isDebugModeEnabled.flow,
|
||||
generalSettings.showAll.flow,
|
||||
) { tick, permissions, pods, isDebugMode, showAll ->
|
||||
bluetoothManager.isBluetoothEnabled,
|
||||
) { _, permissions, pods, isDebugMode, showAll, isBluetoothEnabled ->
|
||||
val items = mutableListOf<OverviewAdapter.Item>()
|
||||
|
||||
val mainPod = podMonitor.mainDevice.first()
|
||||
val now = Instant.now()
|
||||
pods
|
||||
.map {
|
||||
|
||||
permissions
|
||||
.map { perm ->
|
||||
PermissionCardVH.Item(
|
||||
permission = perm,
|
||||
onRequest = { requestPermissionEvent.postValue(it) },
|
||||
)
|
||||
}
|
||||
.run { items.addAll(this) }
|
||||
|
||||
if (permissions.isEmpty()) {
|
||||
if (!isBluetoothEnabled) {
|
||||
items.add(0, BluetoothDisabledVH.Item)
|
||||
} else if (mainPod == null) {
|
||||
items.add(0, MissingMainDeviceVH.Item)
|
||||
}
|
||||
}
|
||||
|
||||
if (permissions.isEmpty() && isBluetoothEnabled) {
|
||||
pods.map {
|
||||
val now = Instant.now()
|
||||
when (it) {
|
||||
is DualApplePods -> DualApplePodsCardVH.Item(
|
||||
now = now,
|
||||
@@ -140,24 +157,7 @@ class OverviewFragmentVM @Inject constructor(
|
||||
isMainPod = it == mainPod,
|
||||
)
|
||||
}
|
||||
}
|
||||
.run { items.addAll(this) }
|
||||
|
||||
permissions
|
||||
.map {
|
||||
PermissionCardVH.Item(
|
||||
permission = it,
|
||||
onRequest = { requestPermissionEvent.postValue(it) }
|
||||
)
|
||||
}
|
||||
.run { items.addAll(this) }
|
||||
|
||||
if (!showAll && items.none { it is PodDeviceVH.Item } && permissions.isEmpty()) {
|
||||
NoPairedDeviceCardVH.Item {
|
||||
generalSettings.showAll.value = true
|
||||
}.run { items.add(this) }
|
||||
} else if (showAll && mainPod == null && permissions.isEmpty()) {
|
||||
items.add(0, MissingMainDeviceVH.Item)
|
||||
}.run { items.addAll(this) }
|
||||
}
|
||||
|
||||
items
|
||||
@@ -166,7 +166,7 @@ class OverviewFragmentVM @Inject constructor(
|
||||
.asLiveData2()
|
||||
|
||||
fun onPermissionResult(granted: Boolean) {
|
||||
if (granted) permissionCheckTrigger.value = UUID.randomUUID()
|
||||
if (granted) permissionTool.recheck()
|
||||
}
|
||||
|
||||
fun goToSettings() = launch {
|
||||
|
||||
+8
-10
@@ -4,29 +4,27 @@ import android.view.ViewGroup
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.lists.binding
|
||||
import eu.darken.capod.common.lists.differ.DifferItem
|
||||
import eu.darken.capod.databinding.OverviewNopaireddeviceItemBinding
|
||||
import eu.darken.capod.databinding.OverviewBluetoothDisabledItemBinding
|
||||
import eu.darken.capod.main.ui.overview.OverviewAdapter
|
||||
|
||||
class NoPairedDeviceCardVH(parent: ViewGroup) :
|
||||
OverviewAdapter.BaseVH<NoPairedDeviceCardVH.Item, OverviewNopaireddeviceItemBinding>(
|
||||
R.layout.overview_nopaireddevice_item,
|
||||
class BluetoothDisabledVH(parent: ViewGroup) :
|
||||
OverviewAdapter.BaseVH<BluetoothDisabledVH.Item, OverviewBluetoothDisabledItemBinding>(
|
||||
R.layout.overview_bluetooth_disabled_item,
|
||||
parent
|
||||
) {
|
||||
|
||||
override val viewBinding = lazy {
|
||||
OverviewNopaireddeviceItemBinding.bind(itemView)
|
||||
OverviewBluetoothDisabledItemBinding.bind(itemView)
|
||||
}
|
||||
|
||||
override val onBindData: OverviewNopaireddeviceItemBinding.(
|
||||
override val onBindData: OverviewBluetoothDisabledItemBinding.(
|
||||
item: Item,
|
||||
payloads: List<Any>
|
||||
) -> Unit = binding(payload = true) { item ->
|
||||
showAllAction.setOnClickListener { item.onShowAll() }
|
||||
|
||||
}
|
||||
|
||||
data class Item(
|
||||
val onShowAll: () -> Unit
|
||||
) : OverviewAdapter.Item {
|
||||
object Item : OverviewAdapter.Item {
|
||||
override val stableId: Long = Item::class.hashCode().toLong()
|
||||
|
||||
override val payloadProvider: ((DifferItem, DifferItem) -> DifferItem?)
|
||||
@@ -1,7 +1,10 @@
|
||||
package eu.darken.capod.main.ui.overview.cards
|
||||
|
||||
import android.text.Html
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.ViewGroup
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.PrivacyPolicy
|
||||
import eu.darken.capod.common.lists.binding
|
||||
import eu.darken.capod.common.lists.differ.DifferItem
|
||||
import eu.darken.capod.common.permissions.Permission
|
||||
@@ -25,6 +28,12 @@ class PermissionCardVH(parent: ViewGroup) :
|
||||
permissionLabel.setText(item.permission.labelRes)
|
||||
permissionDescription.setText(item.permission.descriptionRes)
|
||||
grantAction.setOnClickListener { item.onRequest(item.permission) }
|
||||
privacyPolicy.apply {
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
val ppText = getString(R.string.settings_privacy_policy_label)
|
||||
val ppLink = PrivacyPolicy.URL
|
||||
text = Html.fromHtml("<html><a href=\"$ppLink\">$ppText</a></html>", 0)
|
||||
}
|
||||
}
|
||||
|
||||
data class Item(
|
||||
|
||||
+3
-5
@@ -7,6 +7,7 @@ import eu.darken.capod.R
|
||||
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.getBatteryDrawable
|
||||
import eu.darken.capod.pods.core.getBatteryLevelHeadset
|
||||
import eu.darken.capod.pods.core.getSignalQuality
|
||||
import eu.darken.capod.pods.core.lastSeenFormatted
|
||||
@@ -38,11 +39,8 @@ class BasicSingleApplePodsCardVH(parent: ViewGroup) :
|
||||
reception.append("\n(${getString(R.string.pods_yours)})")
|
||||
}
|
||||
|
||||
headphones.apply {
|
||||
val sb = StringBuilder(context.getString(R.string.pods_single_headphones_label))
|
||||
sb.append("\n").append(device.getBatteryLevelHeadset(context))
|
||||
text = sb
|
||||
}
|
||||
batteryLabel.text = device.getBatteryLevelHeadset(context)
|
||||
batteryIcon.setImageResource(getBatteryDrawable(device.batteryHeadsetPercent))
|
||||
|
||||
status.apply {
|
||||
val sb = StringBuilder()
|
||||
|
||||
+47
-36
@@ -2,11 +2,11 @@ package eu.darken.capod.main.ui.overview.cards.pods
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isInvisible
|
||||
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
|
||||
@@ -24,12 +24,15 @@ class DualApplePodsCardVH(parent: ViewGroup) :
|
||||
val device = item.device
|
||||
name.apply {
|
||||
val sb = StringBuilder(device.getLabel(context))
|
||||
if (device.deviceColor != DeviceColor.UNKNOWN) {
|
||||
sb.append(" (${device.getDeviceColorLabel(context)})")
|
||||
if (!listOf(DeviceColor.WHITE, DeviceColor.UNKNOWN).contains(device.deviceColor)) {
|
||||
sb.append(" (${device.deviceColor.name})")
|
||||
}
|
||||
text = sb
|
||||
if (item.isMainPod) setTypeface(typeface, Typeface.BOLD)
|
||||
else setTypeface(typeface, Typeface.NORMAL)
|
||||
if (item.showDebug) {
|
||||
append(" [${device.primaryPod.name}]")
|
||||
}
|
||||
}
|
||||
deviceIcon.setImageResource(device.iconRes)
|
||||
|
||||
@@ -40,45 +43,53 @@ class DualApplePodsCardVH(parent: ViewGroup) :
|
||||
reception.append("\n(${getString(R.string.pods_yours)})")
|
||||
}
|
||||
|
||||
podLeft.apply {
|
||||
val sb = StringBuilder(context.getString(R.string.pods_dual_left_label))
|
||||
sb.append("\n").append(device.getBatteryLevelLeftPod(context))
|
||||
when {
|
||||
device.isLeftPodCharging -> sb.append("\n").append(getString(R.string.pods_charging_label))
|
||||
device.isLeftPodInEar -> sb.append("\n").append(getString(R.string.pods_inear_label))
|
||||
else -> {}
|
||||
}
|
||||
text = sb
|
||||
// Left Pod
|
||||
device.apply {
|
||||
podLeftBatteryIcon.setImageResource(getBatteryDrawable(batteryLeftPodPercent))
|
||||
podLeftBatteryLabel.text = getBatteryLevelLeftPod(context)
|
||||
|
||||
podLeftChargingIcon.isInvisible = !isLeftPodCharging
|
||||
podLeftChargingLabel.isInvisible = !isLeftPodCharging
|
||||
|
||||
podLeftMicrophoneIcon.isInvisible = !isLeftPodMicrophone
|
||||
podLeftMicrophoneLabel.isInvisible = !isLeftPodMicrophone
|
||||
|
||||
podLeftWearIcon.isInvisible = !isLeftPodInEar
|
||||
podLeftWearLabel.isInvisible = !isLeftPodInEar
|
||||
}
|
||||
|
||||
podRight.apply {
|
||||
val sb = StringBuilder(getString(R.string.pods_dual_right_label))
|
||||
sb.append("\n").append(device.getBatteryLevelRightPod(context))
|
||||
when {
|
||||
device.isRightPodCharging -> sb.append("\n").append(getString(R.string.pods_charging_label))
|
||||
device.isRightPodInEar -> sb.append("\n").append(getString(R.string.pods_inear_label))
|
||||
else -> {}
|
||||
}
|
||||
text = sb
|
||||
// Right Pod
|
||||
device.apply {
|
||||
podRightBatteryIcon.setImageResource(getBatteryDrawable(batteryRightPodPercent))
|
||||
podRightBatteryLabel.text = getBatteryLevelRightPod(context)
|
||||
|
||||
podRightChargingIcon.isInvisible = !isRightPodCharging
|
||||
podRightChargingLabel.isInvisible = !isRightPodCharging
|
||||
|
||||
podRightMicrophoneIcon.isInvisible = !isRightPodMicrophone
|
||||
podRightMicrophoneLabel.isInvisible = !isRightPodMicrophone
|
||||
|
||||
podRightWearIcon.isInvisible = !isRightPodInEar
|
||||
podRightWearLabel.isInvisible = !isRightPodInEar
|
||||
}
|
||||
|
||||
when (device.microPhonePod) {
|
||||
Pod.LEFT -> podLeft.append("\n(${context.getString(R.string.pods_microphone_label)})")
|
||||
Pod.RIGHT -> podRight.append("\n(${context.getString(R.string.pods_microphone_label)})")
|
||||
}
|
||||
// Case
|
||||
device.apply {
|
||||
podCaseBatteryIcon.setImageResource(getBatteryDrawable(batteryCasePercent))
|
||||
podCaseBatteryLabel.text = getBatteryLevelCase(context)
|
||||
|
||||
podCase.apply {
|
||||
val sb = StringBuilder(getString(R.string.pods_case_label))
|
||||
sb.append("\n").append(device.getBatteryLevelCase(context))
|
||||
if (device.isCaseCharging) sb.append("\n").append(getString(R.string.pods_charging_label))
|
||||
when (device.caseLidState) {
|
||||
LidState.OPEN -> sb.append("\n").append(context.getString(R.string.pods_case_status_open_label))
|
||||
LidState.CLOSED -> sb.append("\n").append(context.getString(R.string.pods_case_status_closed_label))
|
||||
LidState.NOT_IN_CASE,
|
||||
LidState.UNKNOWN -> {
|
||||
}
|
||||
podCaseChargingIcon.isInvisible = !isCaseCharging
|
||||
podCaseChargingLabel.isInvisible = !isCaseCharging
|
||||
|
||||
podCaseLidLabel.text = when (caseLidState) {
|
||||
LidState.OPEN -> context.getString(R.string.pods_case_status_open_label)
|
||||
LidState.CLOSED -> context.getString(R.string.pods_case_status_closed_label)
|
||||
else -> context.getString(R.string.general_value_unknown_label)
|
||||
}
|
||||
text = sb
|
||||
|
||||
val hideInfo = !listOf(LidState.OPEN, LidState.CLOSED).contains(caseLidState)
|
||||
podCaseLidIcon.isInvisible = hideInfo
|
||||
podCaseLidLabel.isInvisible = hideInfo
|
||||
}
|
||||
|
||||
status.apply {
|
||||
|
||||
+10
-10
@@ -3,10 +3,12 @@ package eu.darken.capod.main.ui.overview.cards.pods
|
||||
import android.graphics.Typeface
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isInvisible
|
||||
import eu.darken.capod.R
|
||||
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.getBatteryDrawable
|
||||
import eu.darken.capod.pods.core.getBatteryLevelHeadset
|
||||
import eu.darken.capod.pods.core.getSignalQuality
|
||||
import eu.darken.capod.pods.core.lastSeenFormatted
|
||||
@@ -38,16 +40,14 @@ class SingleApplePodsCardVH(parent: ViewGroup) :
|
||||
reception.append("\n(${getString(R.string.pods_yours)})")
|
||||
}
|
||||
|
||||
headphones.apply {
|
||||
val sb = StringBuilder(context.getString(R.string.pods_single_headphones_label))
|
||||
sb.append("\n").append(device.getBatteryLevelHeadset(context))
|
||||
when {
|
||||
device.isHeadsetBeingCharged -> sb.append("\n").append("Charging")
|
||||
device.isHeadphonesBeingWorn -> sb.append("\n").append("In ear")
|
||||
else -> {}
|
||||
}
|
||||
text = sb
|
||||
}
|
||||
batteryLabel.text = device.getBatteryLevelHeadset(context)
|
||||
batteryIcon.setImageResource(getBatteryDrawable(device.batteryHeadsetPercent))
|
||||
|
||||
chargingIcon.isInvisible = device.isHeadsetBeingCharged
|
||||
chargingLabel.isInvisible = device.isHeadsetBeingCharged
|
||||
|
||||
wearIcon.isInvisible = device.isHeadphonesBeingWorn
|
||||
wearLabel.isInvisible = device.isHeadphonesBeingWorn
|
||||
|
||||
status.apply {
|
||||
val sb = StringBuilder()
|
||||
|
||||
@@ -68,7 +68,7 @@ class SettingsFragment : Fragment2(R.layout.settings_fragment),
|
||||
}
|
||||
|
||||
ui.toolbar.apply {
|
||||
subtitle = BuildConfigWrap.VERSION_DESCRIPTION
|
||||
subtitle = BuildConfigWrap.VERSION_DESCRIPTION_SHORT
|
||||
setNavigationOnClickListener { requireActivity().onBackPressed() }
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import eu.darken.androidstarter.common.preferences.Settings
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.BuildConfigWrap
|
||||
import eu.darken.capod.common.PrivacyPolicy
|
||||
import eu.darken.capod.common.WebpageTool
|
||||
import eu.darken.capod.common.uix.PreferenceFragment2
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
@@ -34,7 +35,11 @@ class SettingsIndexFragment : PreferenceFragment2() {
|
||||
}
|
||||
|
||||
override fun onPreferencesCreated() {
|
||||
findPreference<Preference>("core.changelog")!!.summary = BuildConfigWrap.VERSION_DESCRIPTION
|
||||
findPreference<Preference>("core.changelog")!!.summary = BuildConfigWrap.VERSION_DESCRIPTION_LONG
|
||||
findPreference<Preference>("core.privacy")!!.setOnPreferenceClickListener {
|
||||
webpageTool.open(PrivacyPolicy.URL)
|
||||
true
|
||||
}
|
||||
|
||||
super.onPreferencesCreated()
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ class SupportFragmentVM @Inject constructor(
|
||||
|
||||
bodyInfo.append("--- Infos for the developer ---\n")
|
||||
|
||||
bodyInfo.append("App version: ").append(BuildConfigWrap.VERSION_DESCRIPTION).append("\n")
|
||||
bodyInfo.append("App version: ").append(BuildConfigWrap.VERSION_DESCRIPTION_LONG).append("\n")
|
||||
|
||||
bodyInfo.append("Device: ").append(Build.FINGERPRINT).append("\n")
|
||||
bodyInfo.append("Install ID: ").append(installId.id).append("\n")
|
||||
|
||||
@@ -32,24 +32,25 @@ class PodMonitor @Inject constructor(
|
||||
private val deviceCache = mutableMapOf<PodDevice.Id, PodDevice>()
|
||||
private val cacheLock = Mutex()
|
||||
|
||||
val devices: Flow<List<PodDevice>> = generalSettings.scannerMode.flow
|
||||
.flatMapLatest { scannerMode ->
|
||||
bluetoothManager.isBluetoothEnabled.flatMapLatest { isBluetoothEnabled ->
|
||||
if (isBluetoothEnabled) {
|
||||
bleScanner.scan(scannerMode = scannerMode)
|
||||
} else {
|
||||
log(TAG, WARN) { "Bluetooth is current disabled" }
|
||||
emptyFlow()
|
||||
}
|
||||
val devices: Flow<List<PodDevice>> = bluetoothManager.isBluetoothEnabled
|
||||
.flatMapLatest { isBluetoothEnabled ->
|
||||
if (isBluetoothEnabled) {
|
||||
generalSettings.scannerMode.flow
|
||||
} else {
|
||||
log(TAG, WARN) { "Bluetooth is currently disabled" }
|
||||
emptyFlow()
|
||||
}
|
||||
}
|
||||
.flatMapLatest { bleScanner.scan(scannerMode = it) }
|
||||
.map { result ->
|
||||
// For each address we only want the newest result, upstream may batch data
|
||||
result.groupBy { it.address }
|
||||
.values
|
||||
.map { sameAdrDevs ->
|
||||
val newest = sameAdrDevs.maxByOrNull { it.generatedAtNanos }!!
|
||||
log(TAG, VERBOSE) { "Discarding stale results: ${sameAdrDevs.minus(newest)}" }
|
||||
sameAdrDevs.minus(newest).let {
|
||||
if (it.isNotEmpty()) log(TAG, VERBOSE) { "Discarding stale results: $it" }
|
||||
}
|
||||
newest
|
||||
}
|
||||
}
|
||||
@@ -76,28 +77,22 @@ class PodMonitor @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val aboveThreshold = mutableListOf<PodDevice>()
|
||||
val belowThreshold = mutableListOf<PodDevice>()
|
||||
val minimumSignalQuality = generalSettings.minimumSignalQuality.value
|
||||
pods.values.forEach { device ->
|
||||
if (device.signalQuality > minimumSignalQuality) aboveThreshold.add(device)
|
||||
else belowThreshold.add(device)
|
||||
}
|
||||
val now = Instant.now()
|
||||
aboveThreshold.sortedWith(compareBy<PodDevice> {
|
||||
Duration.between(
|
||||
it.lastSeenAt,
|
||||
now
|
||||
val minimumSignalQuality = generalSettings.minimumSignalQuality.value
|
||||
|
||||
val aboveTresholdSorted = pods.values
|
||||
.filter { it.signalQuality > minimumSignalQuality }
|
||||
.sortedWith(
|
||||
compareBy<PodDevice> { Duration.between(it.lastSeenAt, now) }.thenByDescending { it.rssi }
|
||||
)
|
||||
}.thenByDescending { it.rssi })
|
||||
.plus(belowThreshold.sortedWith(compareBy<PodDevice> {
|
||||
Duration.between(
|
||||
it.lastSeenAt,
|
||||
now
|
||||
)
|
||||
}.thenByDescending { it.rssi }))
|
||||
// pods.values.sortedByDescending { it.rssi }
|
||||
val belowThresholdSorted = pods.values
|
||||
.filter { it.signalQuality <= minimumSignalQuality }
|
||||
.sortedWith(
|
||||
compareBy<PodDevice> { Duration.between(it.lastSeenAt, now) }.thenByDescending { it.rssi }
|
||||
)
|
||||
|
||||
val main = pods.values.determineMainDevice()
|
||||
(aboveTresholdSorted + belowThresholdSorted).sortedByDescending { it == main }
|
||||
}
|
||||
.onStart { emit(emptyList()) }
|
||||
.catch {
|
||||
@@ -106,16 +101,17 @@ class PodMonitor @Inject constructor(
|
||||
.replayingShare(appScope)
|
||||
|
||||
val mainDevice: Flow<PodDevice?>
|
||||
get() = devices.map { devices ->
|
||||
devices.maxByOrNull { it.rssi }?.let filter@{ device ->
|
||||
val minimumSignalQuality = generalSettings.minimumSignalQuality.value
|
||||
get() = devices.map { it.determineMainDevice() }
|
||||
|
||||
generalSettings.mainDeviceModel.value.let {
|
||||
if (device.model != it && it != PodDevice.Model.UNKNOWN) return@filter null
|
||||
}
|
||||
private fun Collection<PodDevice>.determineMainDevice(): PodDevice? =
|
||||
maxByOrNull { it.rssi }?.let filter@{ device ->
|
||||
val minimumSignalQuality = generalSettings.minimumSignalQuality.value
|
||||
|
||||
if (device.signalQuality > minimumSignalQuality) device else null
|
||||
generalSettings.mainDeviceModel.value.let {
|
||||
if (device.model != it && it != PodDevice.Model.UNKNOWN) return@filter null
|
||||
}
|
||||
|
||||
if (device.signalQuality > minimumSignalQuality) device else null
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package eu.darken.capod.monitor.core
|
||||
|
||||
import dagger.Reusable
|
||||
import javax.inject.Inject
|
||||
|
||||
@Reusable
|
||||
class PodSorter @Inject constructor(
|
||||
|
||||
)
|
||||
@@ -76,7 +76,7 @@ class MonitorWorker @AssistedInject constructor(
|
||||
}
|
||||
|
||||
private suspend fun doDoWork() {
|
||||
val permissionsMissingOnStart = permissionTool.missingPermissions()
|
||||
val permissionsMissingOnStart = permissionTool.missingPermissions.first()
|
||||
if (permissionsMissingOnStart.isNotEmpty()) {
|
||||
log(TAG, WARN) { "Aborting, missing permissions: $permissionsMissingOnStart" }
|
||||
return
|
||||
@@ -99,7 +99,7 @@ class MonitorWorker @AssistedInject constructor(
|
||||
|
||||
generalSettings.monitorMode.flow
|
||||
.flatMapLatest { monitorMode ->
|
||||
val missingPermsFlow = permissionTool.missingPermissions()
|
||||
val missingPermsFlow = permissionTool.missingPermissions.first()
|
||||
if (missingPermsFlow.isNotEmpty()) {
|
||||
log(TAG, WARN) { "Aborting, permissions are missing for $monitorMode: $missingPermsFlow" }
|
||||
workerScope.coroutineContext.cancelChildren()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.darken.capod.monitor.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.widget.RemoteViews
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.R
|
||||
@@ -26,38 +27,45 @@ class MonitorNotificationViewFactory @Inject constructor(
|
||||
context.packageName,
|
||||
R.layout.monitor_notification_dual_pods_small
|
||||
).apply {
|
||||
device.getBatteryLevelLeftPod(context)
|
||||
.let { if (device.isLeftPodCharging) "$it⚡" else it }
|
||||
.run { setTextViewText(R.id.pod_left, this) }
|
||||
device.apply {
|
||||
// Left
|
||||
setTextViewText(R.id.pod_left_label, getBatteryLevelLeftPod(context))
|
||||
setViewVisibility(R.id.pod_left_charging, if (isLeftPodCharging) View.VISIBLE else View.GONE)
|
||||
setViewVisibility(R.id.pod_left_ear, if (isLeftPodInEar) View.VISIBLE else View.GONE)
|
||||
|
||||
device.getBatteryLevelCase(context)
|
||||
.let { if (device.isCaseCharging) "$it⚡" else it }
|
||||
.run { setTextViewText(R.id.pod_case, this) }
|
||||
// Case
|
||||
setTextViewText(R.id.pod_case_label, getBatteryLevelCase(context))
|
||||
setViewVisibility(R.id.pod_case_charging, if (isCaseCharging) View.VISIBLE else View.GONE)
|
||||
|
||||
device.getBatteryLevelRightPod(context)
|
||||
.let { if (device.isRightPodCharging) "$it⚡" else it }
|
||||
.run { setTextViewText(R.id.pod_right, this) }
|
||||
// Right
|
||||
setTextViewText(R.id.pod_right_label, getBatteryLevelRightPod(context))
|
||||
setViewVisibility(R.id.pod_right_charging, if (isRightPodCharging) View.VISIBLE else View.GONE)
|
||||
setViewVisibility(R.id.pod_right_ear, if (isRightPodInEar) View.VISIBLE else View.GONE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSingleApplePods(device: SingleApplePods): RemoteViews = RemoteViews(
|
||||
context.packageName,
|
||||
R.layout.monitor_notification_single_pods_small
|
||||
).apply {
|
||||
setTextViewText(R.id.headphones_label, device.getLabel(context))
|
||||
|
||||
device.getBatteryLevelHeadset(context)
|
||||
.let { if (!device.isHeadsetBeingCharged) "$it⚡" else it }
|
||||
.run { setTextViewText(R.id.headphones, this) }
|
||||
device.apply {
|
||||
setTextViewText(R.id.headphones_label, getLabel(context))
|
||||
setImageViewResource(R.id.headphones_battery_icon, getBatteryDrawable(batteryHeadsetPercent))
|
||||
setTextViewText(R.id.headphones_battery_label, getBatteryLevelHeadset(context))
|
||||
setViewVisibility(R.id.headphones_charging, if (isHeadsetBeingCharged) View.VISIBLE else View.GONE)
|
||||
setViewVisibility(R.id.headphones_worn, if (isHeadphonesBeingWorn) View.VISIBLE else View.GONE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSingleBasicApplePods(device: BasicSingleApplePods): RemoteViews = RemoteViews(
|
||||
context.packageName,
|
||||
R.layout.monitor_notification_single_pods_basic_small
|
||||
).apply {
|
||||
setTextViewText(R.id.headphones_label, device.getLabel(context))
|
||||
|
||||
device.getBatteryLevelHeadset(context)
|
||||
.run { setTextViewText(R.id.headphones, this) }
|
||||
device.apply {
|
||||
setTextViewText(R.id.headphones_label, getLabel(context))
|
||||
setImageViewResource(R.id.headphones_battery_icon, getBatteryDrawable(batteryHeadsetPercent))
|
||||
setTextViewText(R.id.headphones_battery_label, getBatteryLevelHeadset(context))
|
||||
}
|
||||
}
|
||||
|
||||
private fun createUnknownDevice(device: PodDevice): RemoteViews = RemoteViews(
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package eu.darken.capod.pods.core
|
||||
|
||||
interface HasDualEarDetection : HasEarDetection {
|
||||
|
||||
val isLeftPodInEar: Boolean
|
||||
|
||||
val isRightPodInEar: Boolean
|
||||
|
||||
val isEitherPodInEar: Boolean
|
||||
get() = isLeftPodInEar || isRightPodInEar
|
||||
|
||||
override val isBeingWorn: Boolean
|
||||
get() = isLeftPodInEar && isRightPodInEar
|
||||
|
||||
}
|
||||
@@ -20,8 +20,10 @@ interface PodDevice {
|
||||
|
||||
val scanResult: BleScanResult
|
||||
|
||||
val rssiHistory: List<Int>
|
||||
|
||||
val rssi: Int
|
||||
get() = scanResult.rssi
|
||||
get() = rssiHistory.sum() / rssiHistory.size
|
||||
|
||||
/**
|
||||
* This is not correct but it works ¯\_(ツ)_/¯
|
||||
|
||||
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core
|
||||
|
||||
import android.content.Context
|
||||
import android.icu.text.RelativeDateTimeFormatter
|
||||
import androidx.annotation.DrawableRes
|
||||
import eu.darken.capod.R
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
@@ -28,6 +29,19 @@ fun PodDevice.getSignalQuality(context: Context): String {
|
||||
return "~${percentage.roundToInt()}%"
|
||||
}
|
||||
|
||||
@DrawableRes
|
||||
fun getBatteryDrawable(percent: Float?): Int = when {
|
||||
percent == null -> R.drawable.ic_baseline_battery_unknown_24
|
||||
percent > 0.95f -> R.drawable.ic_baseline_battery_full_24
|
||||
percent > 0.80f -> R.drawable.ic_baseline_battery_6_bar_24
|
||||
percent > 0.65f -> R.drawable.ic_baseline_battery_5_bar_24
|
||||
percent > 0.50f -> R.drawable.ic_baseline_battery_4_bar_24
|
||||
percent > 0.35f -> R.drawable.ic_baseline_battery_3_bar_24
|
||||
percent > 0.20f -> R.drawable.ic_baseline_battery_2_bar_24
|
||||
percent > 0.05f -> R.drawable.ic_baseline_battery_1_bar_24
|
||||
else -> R.drawable.ic_baseline_battery_0_bar_24
|
||||
}
|
||||
|
||||
private val lastSeenFormatter = RelativeDateTimeFormatter.getInstance()
|
||||
|
||||
fun PodDevice.lastSeenFormatted(now: Instant): String {
|
||||
|
||||
@@ -57,24 +57,27 @@ class AppleFactory @Inject constructor(
|
||||
|
||||
log(TAG, INFO) { "Decoding $scanResult" }
|
||||
|
||||
var factory = podFactories.firstOrNull { it.isResponsible(pm) }
|
||||
val factory = podFactories.firstOrNull { it.isResponsible(pm) }
|
||||
|
||||
if (factory == null) {
|
||||
val device = (factory ?: unknownAppleFactory).create(
|
||||
scanResult = scanResult,
|
||||
proximityMessage = pm,
|
||||
)
|
||||
|
||||
if (factory == null && !SILENCED_PMS.contains(device.rawDeviceModel) && scanResult.address != "6E:9E:D1:49:D2:6D") {
|
||||
SILENCED_PMS.add(device.rawDeviceModel)
|
||||
Bugs.report(
|
||||
tag = TAG,
|
||||
message = "Unknown proximity message type",
|
||||
exception = IllegalArgumentException("Unknown ProximityMessage: $pm")
|
||||
)
|
||||
factory = unknownAppleFactory
|
||||
}
|
||||
|
||||
return factory.create(
|
||||
scanResult = scanResult,
|
||||
proximityMessage = pm,
|
||||
)
|
||||
return@withLock device
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val SILENCED_PMS = mutableSetOf<UShort>()
|
||||
private val TAG = logTag("Pod", "Apple", "Factory")
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,9 @@ import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.DEBUG
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.lowerNibble
|
||||
import eu.darken.capod.common.toHex
|
||||
import eu.darken.capod.common.upperNibble
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
@@ -27,11 +30,20 @@ interface ApplePods : PodDevice {
|
||||
val rawStatus: UByte
|
||||
get() = proximityMessage.data[3]
|
||||
|
||||
val rawStatusHex: String
|
||||
get() = rawStatus.toHex()
|
||||
|
||||
val rawPodsBattery: UByte
|
||||
get() = proximityMessage.data[4]
|
||||
|
||||
val rawCaseBattery: UByte
|
||||
get() = proximityMessage.data[5]
|
||||
val rawPodsBatteryHex: String
|
||||
get() = rawPodsBattery.toHex()
|
||||
|
||||
val rawFlags: UShort
|
||||
get() = proximityMessage.data[5].upperNibble
|
||||
|
||||
val rawCaseBattery: UShort
|
||||
get() = proximityMessage.data[5].lowerNibble
|
||||
|
||||
val rawCaseLidState: UByte
|
||||
get() = proximityMessage.data[6]
|
||||
@@ -55,6 +67,7 @@ interface ApplePods : PodDevice {
|
||||
val identifier: PodDevice.Id,
|
||||
val scanResult: BleScanResult,
|
||||
val message: ProximityPairing.Message,
|
||||
val rssiHistory: List<Int> = emptyList()
|
||||
) {
|
||||
val address: String
|
||||
get() = scanResult.address
|
||||
@@ -73,46 +86,46 @@ interface ApplePods : PodDevice {
|
||||
|
||||
private val knownDevs = mutableMapOf<PodDevice.Id, KnownDevice>()
|
||||
|
||||
internal fun recognizeDevice(scanResult: BleScanResult, message: ProximityPairing.Message): PodDevice.Id {
|
||||
internal fun recognizeDevice(scanResult: BleScanResult, message: ProximityPairing.Message): KnownDevice {
|
||||
val address = scanResult.address
|
||||
|
||||
var identifier: PodDevice.Id? = null
|
||||
var recognizedDevice: KnownDevice? = knownDevs.values
|
||||
.firstOrNull { it.address == address }
|
||||
?.also { log(tag, VERBOSE) { "recognizeDevice: Recovered previous ID via address: $it" } }
|
||||
|
||||
knownDevs.values.firstOrNull { it.address == address }?.let {
|
||||
log(tag, VERBOSE) { "recognizeDevice: Recovered previous ID via address: $it" }
|
||||
knownDevs[it.identifier] = it.copy(
|
||||
scanResult = scanResult,
|
||||
message = message,
|
||||
)
|
||||
identifier = it.identifier
|
||||
}
|
||||
|
||||
if (identifier == null) {
|
||||
if (recognizedDevice == null) {
|
||||
val currentMarkers = message.getRecogMarkers()
|
||||
knownDevs.values
|
||||
recognizedDevice = knownDevs.values
|
||||
.firstOrNull { it.message.getRecogMarkers() == currentMarkers }
|
||||
?.let {
|
||||
?.also {
|
||||
log(tag, DEBUG) { "recognizeDevice: Close match based similarity markers." }
|
||||
log(tag, DEBUG) { "recognizeDevice: Old marker: ${it.message.getRecogMarkers()}" }
|
||||
log(tag, DEBUG) { "recognizeDevice: New marker: $currentMarkers" }
|
||||
knownDevs[it.identifier] = it.copy(
|
||||
scanResult = scanResult,
|
||||
message = message,
|
||||
)
|
||||
identifier = it.identifier
|
||||
}
|
||||
}
|
||||
|
||||
if (identifier == null) {
|
||||
recognizedDevice = if (recognizedDevice == null) {
|
||||
log(tag, VERBOSE) { "recognizeDevice: Mapping as new device" }
|
||||
identifier = PodDevice.Id()
|
||||
KnownDevice(
|
||||
identifier = PodDevice.Id(),
|
||||
scanResult = scanResult,
|
||||
message = message,
|
||||
rssiHistory = listOf(scanResult.rssi)
|
||||
)
|
||||
} else {
|
||||
recognizedDevice.copy(
|
||||
scanResult = scanResult,
|
||||
message = message,
|
||||
rssiHistory = recognizedDevice.rssiHistory
|
||||
.toMutableList()
|
||||
.let {
|
||||
if (it.size > 2) it.removeAt(0)
|
||||
it.plus(scanResult.rssi)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
knownDevs[identifier!!] = KnownDevice(
|
||||
identifier = identifier!!,
|
||||
scanResult = scanResult,
|
||||
message = message
|
||||
)
|
||||
knownDevs[recognizedDevice.identifier] = recognizedDevice
|
||||
|
||||
knownDevs.values.toList().forEach { knownDevice ->
|
||||
if (knownDevice.isOlderThan(Duration.ofSeconds(20))) {
|
||||
@@ -121,7 +134,7 @@ interface ApplePods : PodDevice {
|
||||
}
|
||||
}
|
||||
|
||||
return identifier!!
|
||||
return recognizedDevice
|
||||
}
|
||||
|
||||
data class ModelInfo(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Range
|
||||
import androidx.annotation.StringRes
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
@@ -9,23 +8,30 @@ import eu.darken.capod.common.isBitSet
|
||||
import eu.darken.capod.common.lowerNibble
|
||||
import eu.darken.capod.common.upperNibble
|
||||
import eu.darken.capod.pods.core.HasCase
|
||||
import eu.darken.capod.pods.core.HasDualEarDetection
|
||||
import eu.darken.capod.pods.core.HasDualPods
|
||||
import eu.darken.capod.pods.core.HasDualPods.Pod
|
||||
import eu.darken.capod.pods.core.HasEarDetection
|
||||
|
||||
interface DualApplePods : ApplePods, HasDualPods, HasEarDetection, HasCase {
|
||||
interface DualApplePods : ApplePods, HasDualPods, HasDualEarDetection, HasCase {
|
||||
|
||||
val microPhonePod: Pod
|
||||
val primaryPod: Pod
|
||||
get() = when (rawStatus.isBitSet(5)) {
|
||||
true -> Pod.LEFT
|
||||
false -> Pod.RIGHT
|
||||
}
|
||||
|
||||
/**
|
||||
* Normally values for the left pod are in the lower nibbles, if the left pod is primary (microphone)
|
||||
* If the right pod is the primary, the values are flipped.
|
||||
*/
|
||||
val areValuesFlipped: Boolean
|
||||
get() = !rawStatus.isBitSet(5)
|
||||
|
||||
override val batteryLeftPodPercent: Float?
|
||||
get() {
|
||||
val value = when (microPhonePod) {
|
||||
Pod.LEFT -> rawPodsBattery.lowerNibble.toInt()
|
||||
Pod.RIGHT -> rawPodsBattery.upperNibble.toInt()
|
||||
val value = when (areValuesFlipped) {
|
||||
true -> rawPodsBattery.upperNibble.toInt()
|
||||
false -> rawPodsBattery.lowerNibble.toInt()
|
||||
}
|
||||
return when (value) {
|
||||
15 -> null
|
||||
@@ -40,9 +46,9 @@ interface DualApplePods : ApplePods, HasDualPods, HasEarDetection, HasCase {
|
||||
|
||||
override val batteryRightPodPercent: Float?
|
||||
get() {
|
||||
val value = when (microPhonePod) {
|
||||
Pod.LEFT -> rawPodsBattery.upperNibble.toInt()
|
||||
Pod.RIGHT -> rawPodsBattery.lowerNibble.toInt()
|
||||
val value = when (areValuesFlipped) {
|
||||
true -> rawPodsBattery.lowerNibble.toInt()
|
||||
false -> rawPodsBattery.upperNibble.toInt()
|
||||
}
|
||||
return when (value) {
|
||||
15 -> null
|
||||
@@ -55,35 +61,55 @@ interface DualApplePods : ApplePods, HasDualPods, HasEarDetection, HasCase {
|
||||
}
|
||||
}
|
||||
|
||||
val isLeftPodInEar: Boolean
|
||||
get() = when (microPhonePod) {
|
||||
Pod.LEFT -> rawStatus.isBitSet(1)
|
||||
Pod.RIGHT -> rawStatus.isBitSet(3)
|
||||
val isThisPodInThecase: Boolean
|
||||
get() = rawStatus.isBitSet(6)
|
||||
|
||||
val isOnePodInCase: Boolean
|
||||
get() = rawStatus.isBitSet(4)
|
||||
|
||||
val areBothPodsInCase: Boolean
|
||||
get() = rawStatus.isBitSet(2)
|
||||
|
||||
override val isLeftPodInEar: Boolean
|
||||
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||
true -> rawStatus.isBitSet(3)
|
||||
false -> rawStatus.isBitSet(1)
|
||||
}
|
||||
|
||||
val isRightPodInEar: Boolean
|
||||
get() = when (microPhonePod) {
|
||||
Pod.LEFT -> rawStatus.isBitSet(3)
|
||||
Pod.RIGHT -> rawStatus.isBitSet(1)
|
||||
override val isRightPodInEar: Boolean
|
||||
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||
true -> rawStatus.isBitSet(1)
|
||||
false -> rawStatus.isBitSet(3)
|
||||
}
|
||||
|
||||
override val isBeingWorn: Boolean
|
||||
get() = isLeftPodInEar && isRightPodInEar
|
||||
/**
|
||||
* The data flip bit is set if the left pod is primary.
|
||||
* For the pod that is in the case, this is flipped again though.
|
||||
*/
|
||||
val isLeftPodMicrophone: Boolean
|
||||
get() = rawStatus.isBitSet(5) xor isThisPodInThecase
|
||||
|
||||
/**
|
||||
* The data flip bit is UNset if the right pod is primary.
|
||||
* For the pod that is in the case, this is flipped again though.
|
||||
*/
|
||||
val isRightPodMicrophone: Boolean
|
||||
get() = !rawStatus.isBitSet(5) xor isThisPodInThecase
|
||||
|
||||
val isLeftPodCharging: Boolean
|
||||
get() = when (microPhonePod) {
|
||||
Pod.LEFT -> rawCaseBattery.upperNibble.isBitSet(0)
|
||||
Pod.RIGHT -> rawCaseBattery.upperNibble.isBitSet(1)
|
||||
get() = when (areValuesFlipped) {
|
||||
false -> rawFlags.isBitSet(0)
|
||||
true -> rawFlags.isBitSet(1)
|
||||
}
|
||||
|
||||
val isRightPodCharging: Boolean
|
||||
get() = when (microPhonePod) {
|
||||
Pod.LEFT -> rawCaseBattery.upperNibble.isBitSet(1)
|
||||
Pod.RIGHT -> rawCaseBattery.upperNibble.isBitSet(0)
|
||||
get() = when (areValuesFlipped) {
|
||||
false -> rawFlags.isBitSet(1)
|
||||
true -> rawFlags.isBitSet(0)
|
||||
}
|
||||
|
||||
override val batteryCasePercent: Float?
|
||||
get() = when (val value = rawCaseBattery.lowerNibble.toInt()) {
|
||||
get() = when (val value = rawCaseBattery.toInt()) {
|
||||
15 -> null
|
||||
else -> if (value > 10) {
|
||||
log { "Case: Above 100% battery: $value" }
|
||||
@@ -94,11 +120,12 @@ interface DualApplePods : ApplePods, HasDualPods, HasEarDetection, HasCase {
|
||||
}
|
||||
|
||||
override val isCaseCharging: Boolean
|
||||
get() = rawCaseBattery.upperNibble.isBitSet(2)
|
||||
get() = rawFlags.isBitSet(2)
|
||||
|
||||
val caseLidState: LidState
|
||||
get() {
|
||||
return LidState.values().firstOrNull { it.rawRange.contains(rawCaseLidState) } ?: LidState.UNKNOWN
|
||||
val rawstate = rawCaseLidState
|
||||
return LidState.values().firstOrNull { it.rawRange.contains(rawstate.toInt()) } ?: LidState.UNKNOWN
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,39 +136,34 @@ interface DualApplePods : ApplePods, HasDualPods, HasEarDetection, HasCase {
|
||||
* They reset after some time to their start values.
|
||||
* The upper limits are not the maximums but are only reached if playing with the case.
|
||||
*/
|
||||
enum class LidState(val rawRange: Range<UByte>) {
|
||||
OPEN(0x30, 0x37),
|
||||
CLOSED(0x38, 0x3F),
|
||||
NOT_IN_CASE(0x00, 0x03),
|
||||
UNKNOWN(0xFF, 0xFF);
|
||||
|
||||
constructor(vararg raw: Int) : this(Range(raw[0].toUByte(), (raw[1].toUByte())))
|
||||
enum class LidState(val rawRange: IntRange) {
|
||||
OPEN(0x30..0x37),
|
||||
CLOSED(0x38..0x3F),
|
||||
NOT_IN_CASE(0x00..0x03),
|
||||
UNKNOWN(0xFF..0xFF);
|
||||
}
|
||||
|
||||
val deviceColor: DeviceColor
|
||||
get() = DeviceColor.values().firstOrNull { it.raw == rawDeviceColor } ?: DeviceColor.UNKNOWN
|
||||
|
||||
enum class DeviceColor(val raw: UByte?) {
|
||||
|
||||
fun getDeviceColorLabel(context: Context): String = context.getString(deviceColor.labelRes)
|
||||
WHITE(0x00),
|
||||
BLACK(0x01),
|
||||
RED(0x02),
|
||||
BLUE(0x03),
|
||||
PINK(0x04),
|
||||
GRAY(0x05),
|
||||
SILVER(0x06),
|
||||
GOLD(0x07),
|
||||
ROSE_GOLD(0x08),
|
||||
SPACE_GRAY(0x09),
|
||||
DARK_BLUE(0x0a),
|
||||
LIGHT_BLUE(0x0b),
|
||||
YELLOW(0x0c),
|
||||
UNKNOWN(null);
|
||||
|
||||
enum class DeviceColor(val raw: UByte?, @StringRes val labelRes: Int) {
|
||||
|
||||
WHITE(0x00, R.string.pods_device_color_white_label),
|
||||
BLACK(0x01, R.string.pods_device_color_black_label),
|
||||
RED(0x02, R.string.pods_device_color_red_label),
|
||||
BLUE(0x03, R.string.pods_device_color_blue_label),
|
||||
PINK(0x04, R.string.pods_device_color_pink_label),
|
||||
GRAY(0x05, R.string.pods_device_color_gray_label),
|
||||
SILVER(0x06, R.string.pods_device_color_silver_label),
|
||||
GOLD(0x07, R.string.pods_device_color_gold_label),
|
||||
ROSE_GOLD(0x08, R.string.pods_device_color_rose_gold_label),
|
||||
SPACE_GRAY(0x09, R.string.pods_device_color_space_gray_label),
|
||||
DARK_BLUE(0x0a, R.string.pods_device_color_dark_blue_label),
|
||||
LIGHT_BLUE(0x0b, R.string.pods_device_color_light_blue_label),
|
||||
YELLOW(0x0c, R.string.pods_device_color_yellow_label),
|
||||
UNKNOWN(null, R.string.general_value_unknown_label);
|
||||
|
||||
constructor(raw: Int, @StringRes labelRes: Int) : this(raw.toUByte(), labelRes)
|
||||
constructor(raw: Int) : this(raw.toUByte())
|
||||
}
|
||||
|
||||
val connectionState: ConnectionState
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import eu.darken.capod.common.isBitSet
|
||||
import eu.darken.capod.common.upperNibble
|
||||
import eu.darken.capod.pods.core.HasEarDetection
|
||||
import eu.darken.capod.pods.core.HasSinglePod
|
||||
|
||||
@@ -11,7 +10,7 @@ interface SingleApplePods : BasicSingleApplePods, HasEarDetection, HasSinglePod
|
||||
get() = rawStatus.isBitSet(1)
|
||||
|
||||
val isHeadsetBeingCharged: Boolean
|
||||
get() = rawCaseBattery.upperNibble.isBitSet(0)
|
||||
get() = rawFlags.isBitSet(0)
|
||||
|
||||
override val isBeingWorn: Boolean
|
||||
get() = isHeadphonesBeingWorn
|
||||
|
||||
@@ -13,7 +13,8 @@ data class UnknownAppleDevice(
|
||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||
override val lastSeenAt: Instant = Instant.now(),
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val rssiHistory: List<Int>
|
||||
) : ApplePods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.UNKNOWN
|
||||
@@ -27,12 +28,13 @@ data class UnknownAppleDevice(
|
||||
scanResult: BleScanResult,
|
||||
proximityMessage: ProximityPairing.Message,
|
||||
): ApplePods {
|
||||
val identifier = recognizeDevice(scanResult, proximityMessage)
|
||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
||||
|
||||
return UnknownAppleDevice(
|
||||
identifier = identifier,
|
||||
identifier = recognized.identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = proximityMessage,
|
||||
rssiHistory = recognized.rssiHistory,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ data class AirPodsGen1 constructor(
|
||||
override val lastSeenAt: Instant = Instant.now(),
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
private val cachedBatteryPercentage: Float?
|
||||
private val cachedBatteryPercentage: Float?,
|
||||
override val rssiHistory: List<Int>
|
||||
) : DualApplePods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN1
|
||||
@@ -28,16 +29,17 @@ data class AirPodsGen1 constructor(
|
||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
val identifier = recognizeDevice(scanResult, proximityMessage)
|
||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
||||
|
||||
val device = AirPodsGen1(
|
||||
identifier = identifier,
|
||||
identifier = recognized.identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = proximityMessage,
|
||||
cachedBatteryPercentage = cachedValues[identifier]?.caseBatteryPercentage
|
||||
cachedBatteryPercentage = cachedValues[recognized.identifier]?.caseBatteryPercentage,
|
||||
rssiHistory = recognized.rssiHistory,
|
||||
)
|
||||
|
||||
cachedValues[identifier] = ValueCache(
|
||||
cachedValues[recognized.identifier] = ValueCache(
|
||||
caseBatteryPercentage = device.batteryCasePercent
|
||||
)
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ data class AirPodsGen2 constructor(
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
private val cachedBatteryPercentage: Float?,
|
||||
override val rssiHistory: List<Int>,
|
||||
) : DualApplePods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN2
|
||||
@@ -28,16 +29,17 @@ data class AirPodsGen2 constructor(
|
||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
val identifier = recognizeDevice(scanResult, proximityMessage)
|
||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
||||
|
||||
val device = AirPodsGen2(
|
||||
identifier = identifier,
|
||||
identifier = recognized.identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = proximityMessage,
|
||||
cachedBatteryPercentage = cachedValues[identifier]?.caseBatteryPercentage
|
||||
cachedBatteryPercentage = cachedValues[recognized.identifier]?.caseBatteryPercentage,
|
||||
rssiHistory = recognized.rssiHistory,
|
||||
)
|
||||
|
||||
cachedValues[identifier] = ValueCache(
|
||||
cachedValues[recognized.identifier] = ValueCache(
|
||||
caseBatteryPercentage = device.batteryCasePercent
|
||||
)
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ data class AirPodsGen3 constructor(
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
private val cachedBatteryPercentage: Float?,
|
||||
override val rssiHistory: List<Int>,
|
||||
) : DualApplePods {
|
||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN3
|
||||
|
||||
@@ -27,16 +28,17 @@ data class AirPodsGen3 constructor(
|
||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
val identifier = recognizeDevice(scanResult, proximityMessage)
|
||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
||||
|
||||
val device = AirPodsGen3(
|
||||
identifier = identifier,
|
||||
identifier = recognized.identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = proximityMessage,
|
||||
cachedBatteryPercentage = cachedValues[identifier]?.caseBatteryPercentage
|
||||
cachedBatteryPercentage = cachedValues[recognized.identifier]?.caseBatteryPercentage,
|
||||
rssiHistory = recognized.rssiHistory,
|
||||
)
|
||||
|
||||
cachedValues[identifier] = ValueCache(
|
||||
cachedValues[recognized.identifier] = ValueCache(
|
||||
caseBatteryPercentage = device.batteryCasePercent
|
||||
)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ data class AirPodsMax(
|
||||
override val lastSeenAt: Instant = Instant.now(),
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val rssiHistory: List<Int>,
|
||||
) : SingleApplePods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX
|
||||
@@ -24,12 +25,13 @@ data class AirPodsMax(
|
||||
proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
val identifier = recognizeDevice(scanResult, proximityMessage)
|
||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
||||
|
||||
return AirPodsMax(
|
||||
identifier = identifier,
|
||||
identifier = recognized.identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = proximityMessage,
|
||||
rssiHistory = recognized.rssiHistory,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ data class AirPodsPro(
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
private val cachedBatteryPercentage: Float?,
|
||||
override val rssiHistory: List<Int>,
|
||||
) : DualApplePods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO
|
||||
@@ -28,16 +29,17 @@ data class AirPodsPro(
|
||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
val identifier = recognizeDevice(scanResult, proximityMessage)
|
||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
||||
|
||||
val device = AirPodsPro(
|
||||
identifier = identifier,
|
||||
identifier = recognized.identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = proximityMessage,
|
||||
cachedBatteryPercentage = cachedValues[identifier]?.caseBatteryPercentage
|
||||
cachedBatteryPercentage = cachedValues[recognized.identifier]?.caseBatteryPercentage,
|
||||
rssiHistory = recognized.rssiHistory,
|
||||
)
|
||||
|
||||
cachedValues[identifier] = ValueCache(
|
||||
cachedValues[recognized.identifier] = ValueCache(
|
||||
caseBatteryPercentage = device.batteryCasePercent
|
||||
)
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ data class BeatsFlex(
|
||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||
override val lastSeenAt: Instant = Instant.now(),
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val rssiHistory: List<Int>,
|
||||
) : BasicSingleApplePods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_FLEX
|
||||
@@ -24,12 +25,13 @@ data class BeatsFlex(
|
||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
val identifier = recognizeDevice(scanResult, proximityMessage)
|
||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
||||
|
||||
return BeatsFlex(
|
||||
identifier = identifier,
|
||||
identifier = recognized.identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = proximityMessage,
|
||||
rssiHistory = recognized.rssiHistory,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ data class BeatsSolo3(
|
||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||
override val lastSeenAt: Instant = Instant.now(),
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val rssiHistory: List<Int>,
|
||||
) : BasicSingleApplePods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_SOLO_3
|
||||
@@ -24,12 +25,13 @@ data class BeatsSolo3(
|
||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
val identifier = recognizeDevice(scanResult, proximityMessage)
|
||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
||||
|
||||
return BeatsSolo3(
|
||||
identifier = identifier,
|
||||
identifier = recognized.identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = proximityMessage,
|
||||
rssiHistory = recognized.rssiHistory,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ data class BeatsStudio3(
|
||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||
override val lastSeenAt: Instant = Instant.now(),
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val rssiHistory: List<Int>,
|
||||
) : BasicSingleApplePods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_STUDIO_3
|
||||
@@ -24,12 +25,13 @@ data class BeatsStudio3(
|
||||
proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
val identifier = recognizeDevice(scanResult, proximityMessage)
|
||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
||||
|
||||
return BeatsStudio3(
|
||||
identifier = identifier,
|
||||
identifier = recognized.identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = proximityMessage,
|
||||
rssiHistory = recognized.rssiHistory,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ data class BeatsX(
|
||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||
override val lastSeenAt: Instant = Instant.now(),
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val rssiHistory: List<Int>,
|
||||
) : BasicSingleApplePods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_X
|
||||
@@ -24,12 +25,13 @@ data class BeatsX(
|
||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
val identifier = recognizeDevice(scanResult, proximityMessage)
|
||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
||||
|
||||
return BeatsX(
|
||||
identifier = identifier,
|
||||
identifier = recognized.identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = proximityMessage,
|
||||
rssiHistory = recognized.rssiHistory,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ data class PowerBeats3(
|
||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||
override val lastSeenAt: Instant = Instant.now(),
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val rssiHistory: List<Int>,
|
||||
) : BasicSingleApplePods {
|
||||
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_3
|
||||
|
||||
@@ -23,12 +24,13 @@ data class PowerBeats3(
|
||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
val identifier = recognizeDevice(scanResult, proximityMessage)
|
||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
||||
|
||||
return PowerBeats3(
|
||||
identifier = identifier,
|
||||
identifier = recognized.identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = proximityMessage,
|
||||
rssiHistory = recognized.rssiHistory,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ data class PowerBeatsPro(
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
private val cachedBatteryPercentage: Float?,
|
||||
override val rssiHistory: List<Int>,
|
||||
) : DualApplePods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_PRO
|
||||
@@ -28,16 +29,17 @@ data class PowerBeatsPro(
|
||||
proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
val identifier = recognizeDevice(scanResult, proximityMessage)
|
||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
||||
|
||||
val device = PowerBeatsPro(
|
||||
identifier = identifier,
|
||||
identifier = recognized.identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = proximityMessage,
|
||||
cachedBatteryPercentage = cachedValues[identifier]?.caseBatteryPercentage
|
||||
cachedBatteryPercentage = cachedValues[recognized.identifier]?.caseBatteryPercentage,
|
||||
rssiHistory = recognized.rssiHistory,
|
||||
)
|
||||
|
||||
cachedValues[identifier] = ValueCache(
|
||||
cachedValues[recognized.identifier] = ValueCache(
|
||||
caseBatteryPercentage = device.batteryCasePercent
|
||||
)
|
||||
|
||||
|
||||
@@ -19,7 +19,8 @@ object ProximityPairing {
|
||||
val vendor: UByte,
|
||||
val length: UByte,
|
||||
val device: UShort,
|
||||
val batteryData: Set<UByte>,
|
||||
val podBatteryData: Set<UShort>,
|
||||
val caseBatteryData: UShort,
|
||||
val deviceColor: UByte,
|
||||
)
|
||||
|
||||
@@ -28,9 +29,15 @@ object ProximityPairing {
|
||||
length = PROXIMITY_PAIRING_MESSAGE_LENGTH.toUByte(),
|
||||
device = (((data[1].toInt() and 255) shl 8) or (data[2].toInt() and 255)).toUShort(),
|
||||
// Make comparison order independent
|
||||
batteryData = setOf(data[4].upperNibble, data[4].lowerNibble),
|
||||
podBatteryData = setOf(data[4].upperNibble, data[4].lowerNibble),
|
||||
caseBatteryData = data[5].lowerNibble,
|
||||
deviceColor = data[7]
|
||||
)
|
||||
|
||||
override fun toString(): String {
|
||||
val dataHex = data.joinToString(separator = " ") { String.format("%02X", it.toByte()) }
|
||||
return "ProximityPairing.Message(type=$type, length=$length, data=$dataHex)"
|
||||
}
|
||||
}
|
||||
|
||||
@Reusable
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package eu.darken.capod.reaction.autoconnect
|
||||
|
||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
import eu.darken.capod.monitor.core.PodMonitor
|
||||
import eu.darken.capod.pods.core.HasDualEarDetection
|
||||
import eu.darken.capod.pods.core.HasEarDetection
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.reaction.settings.ReactionSettings
|
||||
@@ -22,29 +25,36 @@ class AutoConnect @Inject constructor(
|
||||
) {
|
||||
|
||||
fun monitor(): Flow<Unit> = reactionSettings.autoConnect.flow
|
||||
.flatMapLatest { if (it) podMonitor.mainDevice else emptyFlow() }
|
||||
.filterNotNull()
|
||||
.distinctUntilChanged()
|
||||
.map { podDevice ->
|
||||
log(TAG) { "mainPodDevice is $podDevice" }
|
||||
.flatMapLatest { isAutoConnectEnabled ->
|
||||
if (isAutoConnectEnabled) {
|
||||
combine(
|
||||
bluetoothManager.connectedDevices().distinctUntilChanged(),
|
||||
podMonitor.mainDevice.filterNotNull().distinctUntilChangedBy { it.rawDataHex },
|
||||
) { connectedDevices, mainDevice ->
|
||||
connectedDevices to mainDevice
|
||||
}
|
||||
} else {
|
||||
emptyFlow()
|
||||
}
|
||||
}
|
||||
.map { (connectedDevices, mainDevice) ->
|
||||
log(TAG, VERBOSE) { "mainPodDevice is $mainDevice" }
|
||||
|
||||
val mainDeviceAddr = generalSettings.mainDeviceAddress.value
|
||||
if (mainDeviceAddr.isNullOrEmpty()) {
|
||||
log(TAG) { "mainDeviceAddress is null" }
|
||||
log(TAG, WARN) { "mainDeviceAddress is null" }
|
||||
return@map
|
||||
} else {
|
||||
log(TAG) { "mainDeviceAddress is $mainDeviceAddr" }
|
||||
}
|
||||
|
||||
val bondedDevice = bluetoothManager.bondedDevices().firstOrNull { it.address == mainDeviceAddr }
|
||||
if (bondedDevice == null) {
|
||||
log(TAG) { "No bonded device matches $mainDeviceAddr" }
|
||||
log(TAG, WARN) { "No bonded device matches $mainDeviceAddr" }
|
||||
return@map
|
||||
} else {
|
||||
log(TAG) { "Found target device: $bondedDevice" }
|
||||
log(TAG, VERBOSE) { "Found target device: $bondedDevice" }
|
||||
}
|
||||
|
||||
val isAlreadyConnected = bluetoothManager.connectedDevices().first().any {
|
||||
val isAlreadyConnected = connectedDevices.any {
|
||||
it.address == bondedDevice.address
|
||||
}
|
||||
|
||||
@@ -57,12 +67,18 @@ class AutoConnect @Inject constructor(
|
||||
log(TAG) { "Checking condition $condition" }
|
||||
val conditionFulfilled = when (condition) {
|
||||
AutoConnectCondition.WHEN_SEEN -> true
|
||||
AutoConnectCondition.CASE_OPEN -> when (podDevice) {
|
||||
is DualApplePods -> podDevice.caseLidState == DualApplePods.LidState.OPEN
|
||||
AutoConnectCondition.CASE_OPEN -> when (mainDevice) {
|
||||
is DualApplePods -> mainDevice.caseLidState == DualApplePods.LidState.OPEN
|
||||
else -> true
|
||||
}
|
||||
AutoConnectCondition.IN_EAR -> when (podDevice) {
|
||||
is HasEarDetection -> podDevice.isBeingWorn
|
||||
AutoConnectCondition.IN_EAR -> when (mainDevice) {
|
||||
is HasEarDetection -> {
|
||||
if (mainDevice is HasDualEarDetection && reactionSettings.onePodMode.value) {
|
||||
mainDevice.isEitherPodInEar
|
||||
} else {
|
||||
mainDevice.isBeingWorn
|
||||
}
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||
import eu.darken.capod.common.flow.withPrevious
|
||||
import eu.darken.capod.monitor.core.PodMonitor
|
||||
import eu.darken.capod.pods.core.HasDualEarDetection
|
||||
import eu.darken.capod.pods.core.HasEarDetection
|
||||
import eu.darken.capod.reaction.settings.ReactionSettings
|
||||
import kotlinx.coroutines.flow.*
|
||||
@@ -25,7 +26,8 @@ class PlayPause @Inject constructor(
|
||||
fun monitor() = combine(
|
||||
reactionSettings.autoPlay.flow,
|
||||
reactionSettings.autoPause.flow,
|
||||
) { play, pause -> play || pause }
|
||||
reactionSettings.onePodMode.flow,
|
||||
) { play, pause, _ -> play || pause }
|
||||
.flatMapLatest { if (it) bluetoothManager.connectedDevices() else emptyFlow() }
|
||||
.flatMapLatest {
|
||||
if (it.isEmpty()) {
|
||||
@@ -42,7 +44,28 @@ class PlayPause @Inject constructor(
|
||||
if (previous is HasEarDetection && current is HasEarDetection) {
|
||||
log(TAG, VERBOSE) { "previous=${previous.isBeingWorn}, current=${current.isBeingWorn}" }
|
||||
log(TAG, VERBOSE) { "previous-id=${previous.identifier}, current-id=${current.identifier}" }
|
||||
if (previous.identifier == current.identifier && previous.isBeingWorn != current.isBeingWorn) {
|
||||
if (previous.identifier != current.identifier) {
|
||||
return@onEach
|
||||
}
|
||||
|
||||
if (previous is HasDualEarDetection && current is HasDualEarDetection && reactionSettings.onePodMode.value) {
|
||||
log(TAG) { "Ear status changed for dual pod device in single pod mode." }
|
||||
val previousWorn = previous.isEitherPodInEar
|
||||
val currentWorn = current.isEitherPodInEar
|
||||
if (!previousWorn && currentWorn && !mediaControl.isPlaying) {
|
||||
if (reactionSettings.autoPlay.value) {
|
||||
mediaControl.sendPlay()
|
||||
} else {
|
||||
log(TAG) { "autoPlay is disabled" }
|
||||
}
|
||||
} else if (!currentWorn && previousWorn && mediaControl.isPlaying) {
|
||||
if (reactionSettings.autoPause.value) {
|
||||
mediaControl.sendPause()
|
||||
} else {
|
||||
log(TAG) { "autoPause is disabled" }
|
||||
}
|
||||
}
|
||||
} else if (previous.isBeingWorn != current.isBeingWorn) {
|
||||
log(TAG) { "Ear status changed for monitored device." }
|
||||
if (current.isBeingWorn && !mediaControl.isPlaying) {
|
||||
if (reactionSettings.autoPlay.value) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package eu.darken.capod.reaction.popup
|
||||
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.INFO
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.*
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||
@@ -9,12 +9,12 @@ import eu.darken.capod.common.flow.withPrevious
|
||||
import eu.darken.capod.monitor.core.PodMonitor
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.reaction.popup.ui.PopUpNotification
|
||||
import eu.darken.capod.reaction.popup.ui.PopUpWindow
|
||||
import eu.darken.capod.reaction.settings.ReactionSettings
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -22,16 +22,25 @@ import javax.inject.Singleton
|
||||
class PopUpReaction @Inject constructor(
|
||||
private val podMonitor: PodMonitor,
|
||||
private val reactionSettings: ReactionSettings,
|
||||
private val popUpNotification: PopUpNotification,
|
||||
private val popupWindow: PopUpWindow,
|
||||
private val dispatcherProvider: DispatcherProvider,
|
||||
) {
|
||||
|
||||
private val coolDowns = mutableMapOf<PodDevice.Id, Instant>()
|
||||
|
||||
fun monitor(): Flow<Unit> = reactionSettings.showPopUpOnCaseOpen.flow
|
||||
.flatMapLatest { if (it) podMonitor.mainDevice else emptyFlow() }
|
||||
.flatMapLatest { isEnabled ->
|
||||
if (isEnabled) {
|
||||
podMonitor.mainDevice.distinctUntilChangedBy { it?.rawDataHex }
|
||||
} else {
|
||||
emptyFlow()
|
||||
}
|
||||
}
|
||||
.withPrevious()
|
||||
.setupCommonEventHandlers(TAG) { "monitor" }
|
||||
.map { (previous, current) ->
|
||||
if (previous is DualApplePods? && current is DualApplePods) {
|
||||
log(TAG) {
|
||||
log(TAG, VERBOSE) {
|
||||
val prev = previous?.rawCaseLidState?.let { String.format("%02X", it.toByte()) }
|
||||
val cur = current.rawCaseLidState.let { String.format("%02X", it.toByte()) }
|
||||
"previous=$prev (${previous?.caseLidState}), current=$cur (${current.caseLidState})"
|
||||
@@ -46,17 +55,48 @@ class PopUpReaction @Inject constructor(
|
||||
if (isSameDeviceWithCaseNowOpen || isNewDeviceWithJustOpenedCase) {
|
||||
log(TAG) { "Case lid status changed for monitored device." }
|
||||
|
||||
if (current.caseLidState == DualApplePods.LidState.OPEN && current.model != PodDevice.Model.UNKNOWN) {
|
||||
log(TAG, INFO) { "Show popup" }
|
||||
popUpNotification.showNotification(current)
|
||||
} else if (current.caseLidState != DualApplePods.LidState.OPEN) {
|
||||
log(TAG, INFO) { "Hide popup" }
|
||||
popUpNotification.hideNotification()
|
||||
}
|
||||
tryPopWindow(current)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun tryPopWindow(current: DualApplePods) {
|
||||
if (current.caseLidState == DualApplePods.LidState.OPEN) {
|
||||
log(TAG, INFO) { "Show popup" }
|
||||
|
||||
val now = Instant.now()
|
||||
val lastShown = coolDowns[current.identifier] ?: Instant.MIN
|
||||
val sinceLastPop = Duration.between(lastShown, now)
|
||||
log(TAG) { "Time since last popup: $sinceLastPop" }
|
||||
if (sinceLastPop < Duration.ofSeconds(10)) {
|
||||
log(TAG, INFO) { "Popup is still on cooldown: $sinceLastPop" }
|
||||
return
|
||||
} else {
|
||||
coolDowns[current.identifier] = Instant.now()
|
||||
}
|
||||
|
||||
withContext(dispatcherProvider.Main) {
|
||||
popupWindow.show(current)
|
||||
}
|
||||
} else if (current.caseLidState != DualApplePods.LidState.OPEN) {
|
||||
when (current.caseLidState) {
|
||||
DualApplePods.LidState.CLOSED -> {
|
||||
log(TAG, INFO) { "Lid was actively closed, resetting cooldown." }
|
||||
coolDowns.remove(current.identifier)
|
||||
}
|
||||
else -> {
|
||||
log(TAG, WARN) { "Lid was was not actively closed, refreshing cooldown." }
|
||||
coolDowns[current.identifier] = Instant.now()
|
||||
}
|
||||
}
|
||||
|
||||
log(TAG, INFO) { "Hide popup" }
|
||||
withContext(dispatcherProvider.Main) {
|
||||
popupWindow.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("Reaction", "PopUp")
|
||||
}
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
package eu.darken.capod.reaction.popup.ui
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.BuildConfigWrap
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.notifications.PendingIntentCompat
|
||||
import eu.darken.capod.main.ui.MainActivity
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class PopUpNotification @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val notificationManager: NotificationManager,
|
||||
private val notificationViewFactory: PopUpNotificationViewFactory
|
||||
) {
|
||||
|
||||
private val builder: NotificationCompat.Builder
|
||||
|
||||
init {
|
||||
NotificationChannel(
|
||||
NOTIFICATION_CHANNEL_ID,
|
||||
context.getString(R.string.notification_channel_reaction_popup_label),
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
).run {
|
||||
vibrationPattern = LongArray(1) { 0 }
|
||||
enableVibration(true)
|
||||
enableLights(false)
|
||||
setSound(null, null)
|
||||
notificationManager.createNotificationChannel(this)
|
||||
}
|
||||
|
||||
val openIntent = Intent(context, MainActivity::class.java)
|
||||
val openPi = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
openIntent,
|
||||
PendingIntentCompat.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID).apply {
|
||||
setChannelId(NOTIFICATION_CHANNEL_ID)
|
||||
setContentIntent(openPi)
|
||||
priority = NotificationCompat.PRIORITY_LOW
|
||||
setSmallIcon(R.drawable.ic_device_generic_earbuds)
|
||||
setContentTitle(context.getString(R.string.app_name))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBuilder(device: PodDevice): NotificationCompat.Builder = builder.apply {
|
||||
setStyle(NotificationCompat.DecoratedCustomViewStyle())
|
||||
setCustomContentView(notificationViewFactory.createContentView(device))
|
||||
setSmallIcon(device.iconRes)
|
||||
setSubText(null)
|
||||
}
|
||||
|
||||
fun showNotification(device: PodDevice) {
|
||||
val notification = getBuilder(device).build()
|
||||
log(TAG) { "showNotification(): $device" }
|
||||
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
fun hideNotification() {
|
||||
log(TAG) { "hideNotification()" }
|
||||
notificationManager.cancel(NOTIFICATION_ID)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = logTag("Reaction", "PopUp", "Notifications")
|
||||
private val NOTIFICATION_CHANNEL_ID =
|
||||
"${BuildConfigWrap.APPLICATION_ID}.notification.channel.reaction.popup"
|
||||
internal const val NOTIFICATION_ID = 9000
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package eu.darken.capod.reaction.popup.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.RemoteViews
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.pods.core.*
|
||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class PopUpNotificationViewFactory @Inject constructor(
|
||||
@ApplicationContext private val context: Context
|
||||
) {
|
||||
|
||||
fun createContentView(device: PodDevice): RemoteViews = when (device) {
|
||||
is DualApplePods -> createDualApplePods(device)
|
||||
is SingleApplePods -> createSingleApplePods(device)
|
||||
is BasicSingleApplePods -> createSingleBasicApplePods(device)
|
||||
else -> throw IllegalArgumentException("Unexpected device: $device")
|
||||
}
|
||||
|
||||
private fun createDualApplePods(device: DualApplePods): RemoteViews = RemoteViews(
|
||||
context.packageName,
|
||||
R.layout.popup_notification_dual_pods
|
||||
).apply {
|
||||
setTextViewText(R.id.pod_label, device.getLabel(context))
|
||||
|
||||
device.getBatteryLevelLeftPod(context)
|
||||
.let { if (device.isLeftPodCharging) "$it⚡" else it }
|
||||
.run { setTextViewText(R.id.pod_left, this) }
|
||||
|
||||
device.getBatteryLevelCase(context)
|
||||
.let { if (device.isCaseCharging) "$it⚡" else it }
|
||||
.run { setTextViewText(R.id.pod_case, this) }
|
||||
|
||||
device.getBatteryLevelRightPod(context)
|
||||
.let { if (device.isRightPodCharging) "$it⚡" else it }
|
||||
.run { setTextViewText(R.id.pod_right, this) }
|
||||
}
|
||||
|
||||
private fun createSingleApplePods(device: SingleApplePods): RemoteViews = RemoteViews(
|
||||
context.packageName,
|
||||
R.layout.popup_notification_single_pods
|
||||
).apply {
|
||||
setTextViewText(R.id.headphones_label, device.getLabel(context))
|
||||
|
||||
device.getBatteryLevelHeadset(context)
|
||||
.let { if (!device.isHeadsetBeingCharged) "$it⚡" else it }
|
||||
.run { setTextViewText(R.id.headphones, this) }
|
||||
}
|
||||
|
||||
private fun createSingleBasicApplePods(device: BasicSingleApplePods): RemoteViews = RemoteViews(
|
||||
context.packageName,
|
||||
R.layout.popup_notification_single_pods_basic
|
||||
).apply {
|
||||
setTextViewText(R.id.headphones_label, device.getLabel(context))
|
||||
|
||||
device.getBatteryLevelHeadset(context)
|
||||
.run { setTextViewText(R.id.headphones, this) }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package eu.darken.capod.reaction.popup.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import androidx.core.view.isInvisible
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||
import eu.darken.capod.databinding.PopupNotificationDualPodsBinding
|
||||
import eu.darken.capod.databinding.PopupNotificationSinglePodsBasicBinding
|
||||
import eu.darken.capod.databinding.PopupNotificationSinglePodsBinding
|
||||
import eu.darken.capod.pods.core.*
|
||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class PopUpPodViewFactory @Inject constructor(
|
||||
@ApplicationContext private val appContext: Context,
|
||||
private val debugSettings: DebugSettings,
|
||||
) {
|
||||
|
||||
private val context = ContextThemeWrapper(appContext, R.style.AppTheme)
|
||||
private val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
|
||||
fun createContentView(parent: ViewGroup, device: PodDevice): View = when (device) {
|
||||
is DualApplePods -> createDualApplePods(parent, device)
|
||||
is SingleApplePods -> createSingleApplePods(parent, device) // Unused, has no case to trigger reaction?
|
||||
is BasicSingleApplePods -> createSingleBasicApplePods(
|
||||
parent,
|
||||
device
|
||||
) // Unused, has no case to trigger reaction?
|
||||
else -> throw IllegalArgumentException("Unexpected device: $device")
|
||||
}
|
||||
|
||||
private fun createDualApplePods(parent: ViewGroup, device: DualApplePods): View =
|
||||
PopupNotificationDualPodsBinding.inflate(layoutInflater, parent, false).apply {
|
||||
device.apply {
|
||||
podIcon.setImageResource(iconRes)
|
||||
podLabel.text = getLabel(context)
|
||||
signal.text = getSignalQuality(context)
|
||||
signal.isInvisible = debugSettings.isDebugModeEnabled.value
|
||||
|
||||
// Left
|
||||
podLeftBatteryIcon.setImageResource(getBatteryDrawable(batteryLeftPodPercent))
|
||||
podLeftBatteryLabel.text = getBatteryLevelLeftPod(context)
|
||||
|
||||
// Case
|
||||
podCaseBatteryIcon.setImageResource(getBatteryDrawable(batteryCasePercent))
|
||||
podCaseBatteryLabel.text = getBatteryLevelCase(context)
|
||||
|
||||
// Right
|
||||
podRightBatteryIcon.setImageResource(getBatteryDrawable(batteryRightPodPercent))
|
||||
podRightBatteryLabel.text = getBatteryLevelRightPod(context)
|
||||
}
|
||||
}.root
|
||||
|
||||
private fun createSingleApplePods(parent: ViewGroup, device: SingleApplePods): View =
|
||||
PopupNotificationSinglePodsBinding.inflate(layoutInflater, parent, false).apply {
|
||||
device.apply {
|
||||
headphonesIcon.setImageResource(iconRes)
|
||||
headphonesLabel.text = getLabel(context)
|
||||
signal.text = getSignalQuality(context)
|
||||
signal.isInvisible = debugSettings.isDebugModeEnabled.value
|
||||
|
||||
headphonesBatteryIcon.setImageResource(getBatteryDrawable(batteryHeadsetPercent))
|
||||
headphonesBatteryLabel.text = getBatteryLevelHeadset(context)
|
||||
}
|
||||
}.root
|
||||
|
||||
private fun createSingleBasicApplePods(parent: ViewGroup, device: BasicSingleApplePods): View =
|
||||
PopupNotificationSinglePodsBasicBinding.inflate(layoutInflater, parent, false).apply {
|
||||
device.apply {
|
||||
headphonesIcon.setImageResource(iconRes)
|
||||
headphonesLabel.text = getLabel(context)
|
||||
signal.text = getSignalQuality(context)
|
||||
signal.isInvisible = debugSettings.isDebugModeEnabled.value
|
||||
|
||||
headphonesBatteryIcon.setImageResource(getBatteryDrawable(batteryHeadsetPercent))
|
||||
headphonesBatteryLabel.text = getBatteryLevelHeadset(context)
|
||||
}
|
||||
}.root
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package eu.darken.capod.reaction.popup.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Context.WINDOW_SERVICE
|
||||
import android.graphics.PixelFormat
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.FrameLayout
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.ERROR
|
||||
import eu.darken.capod.common.debug.logging.asLog
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class PopUpWindow @Inject constructor(
|
||||
@ApplicationContext private val appContext: Context,
|
||||
private val podViewFactory: PopUpPodViewFactory
|
||||
) {
|
||||
|
||||
private val context = ContextThemeWrapper(appContext, R.style.AppTheme)
|
||||
private val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
private val windowManager: WindowManager = context.getSystemService(WINDOW_SERVICE) as WindowManager
|
||||
private val layoutParams = WindowManager.LayoutParams(
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
WindowManager.LayoutParams.WRAP_CONTENT, // Display it on top of other application windows
|
||||
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, // Don't let it grab the input focus
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, // Make the underlying application window visible
|
||||
|
||||
PixelFormat.TRANSLUCENT
|
||||
).apply {
|
||||
gravity = Gravity.BOTTOM
|
||||
}
|
||||
private val popUpView: View = layoutInflater.inflate(R.layout.popup_window_container_layout, null).apply {
|
||||
findViewById<View>(R.id.close_action).setOnClickListener { close() }
|
||||
}
|
||||
private val deviceContainer = popUpView.findViewById<FrameLayout>(R.id.popup_content)
|
||||
fun show(device: PodDevice) = try {
|
||||
log(TAG) { "open()" }
|
||||
if (popUpView.windowToken != null || popUpView.parent != null) {
|
||||
log(TAG) { "View already added." }
|
||||
close()
|
||||
}
|
||||
|
||||
val podView = podViewFactory.createContentView(deviceContainer, device)
|
||||
deviceContainer.removeAllViews()
|
||||
deviceContainer.addView(podView)
|
||||
windowManager.addView(popUpView, layoutParams)
|
||||
} catch (e: Exception) {
|
||||
log(TAG, ERROR) { "open() failed: ${e.asLog()}" }
|
||||
}
|
||||
|
||||
fun close() = try {
|
||||
log(TAG) { "close()" }
|
||||
if (popUpView.parent != null) {
|
||||
windowManager.removeView(popUpView)
|
||||
deviceContainer.removeAllViews()
|
||||
} else {
|
||||
log(TAG) { "View was not added" }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log(TAG, ERROR) { "close() failed: ${e.asLog()}" }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("Reaction", "PopUp", "Window")
|
||||
}
|
||||
}
|
||||
@@ -47,11 +47,17 @@ class ReactionSettings @Inject constructor(
|
||||
false
|
||||
)
|
||||
|
||||
val onePodMode = preferences.createFlowPreference(
|
||||
"reaction.onepod.enabled",
|
||||
false
|
||||
)
|
||||
|
||||
override val preferenceDataStore: PreferenceDataStore = PreferenceStoreMapper(
|
||||
autoPause,
|
||||
autoPlay,
|
||||
autoConnect,
|
||||
autoConnectCondition,
|
||||
showPopUpOnCaseOpen,
|
||||
onePodMode,
|
||||
)
|
||||
}
|
||||
@@ -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/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M17,5v16c0,0.55 -0.45,1 -1,1H8c-0.55,0 -1,-0.45 -1,-1V5c0,-0.55 0.45,-1 1,-1h2V2h4v2h2C16.55,4 17,4.45 17,5zM15,6H9v14h6V6z" />
|
||||
</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/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M17,5v16c0,0.55 -0.45,1 -1,1H8c-0.55,0 -1,-0.45 -1,-1V5c0,-0.55 0.45,-1 1,-1h2V2h4v2h2C16.55,4 17,4.45 17,5zM15,6H9v12h6V6z" />
|
||||
</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/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M17,5v16c0,0.55 -0.45,1 -1,1H8c-0.55,0 -1,-0.45 -1,-1V5c0,-0.55 0.45,-1 1,-1h2V2h4v2h2C16.55,4 17,4.45 17,5zM15,6H9v10h6V6z" />
|
||||
</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/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M17,5v16c0,0.55 -0.45,1 -1,1H8c-0.55,0 -1,-0.45 -1,-1V5c0,-0.55 0.45,-1 1,-1h2V2h4v2h2C16.55,4 17,4.45 17,5zM15,6H9v8h6V6z" />
|
||||
</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/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M17,5v16c0,0.55 -0.45,1 -1,1H8c-0.55,0 -1,-0.45 -1,-1V5c0,-0.55 0.45,-1 1,-1h2V2h4v2h2C16.55,4 17,4.45 17,5zM15,6H9v6h6V6z" />
|
||||
</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/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M17,5v16c0,0.55 -0.45,1 -1,1H8c-0.55,0 -1,-0.45 -1,-1V5c0,-0.55 0.45,-1 1,-1h2V2h4v2h2C16.55,4 17,4.45 17,5zM15,6H9v4h6V6z" />
|
||||
</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/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M17,5v16c0,0.55 -0.45,1 -1,1H8c-0.55,0 -1,-0.45 -1,-1V5c0,-0.55 0.45,-1 1,-1h2V2h4v2h2C16.55,4 17,4.45 17,5zM15,6H9v2h6V6z" />
|
||||
</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/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M15.67,4H14V2h-4v2H8.33C7.6,4 7,4.6 7,5.33v15.33C7,21.4 7.6,22 8.33,22h7.33c0.74,0 1.34,-0.6 1.34,-1.33V5.33C17,4.6 16.4,4 15.67,4z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<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/colorControlNormal"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M15.67,4L14,4L14,2h-4v2L8.33,4C7.6,4 7,4.6 7,5.33v15.33C7,21.4 7.6,22 8.33,22h7.33c0.74,0 1.34,-0.6 1.34,-1.33L17,5.33C17,4.6 16.4,4 15.67,4zM12.95,17.95h-1.9v-1.9h1.9v1.9zM14.3,12.69s-0.38,0.42 -0.67,0.71c-0.48,0.48 -0.83,1.15 -0.83,1.6h-1.6c0,-0.83 0.46,-1.52 0.93,-2l0.93,-0.94c0.27,-0.27 0.44,-0.65 0.44,-1.06 0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5L9,11c0,-1.66 1.34,-3 3,-3s3,1.34 3,3c0,0.66 -0.27,1.26 -0.7,1.69z" />
|
||||
</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/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<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/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M3,3v8h8L11,3L3,3zM9,9L5,9L5,5h4v4zM3,13v8h8v-8L3,13zM9,19L5,19v-4h4v4zM13,3v8h8L21,3h-8zM19,9h-4L15,5h4v4zM13,13v8h8v-8h-8zM19,19h-4v-4h4v4z"
|
||||
android:fillType="evenOdd" />
|
||||
</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/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M17,20c-0.29,0 -0.56,-0.06 -0.76,-0.15 -0.71,-0.37 -1.21,-0.88 -1.71,-2.38 -0.51,-1.56 -1.47,-2.29 -2.39,-3 -0.79,-0.61 -1.61,-1.24 -2.32,-2.53C9.29,10.98 9,9.93 9,9c0,-2.8 2.2,-5 5,-5s5,2.2 5,5h2c0,-3.93 -3.07,-7 -7,-7S7,5.07 7,9c0,1.26 0.38,2.65 1.07,3.9 0.91,1.65 1.98,2.48 2.85,3.15 0.81,0.62 1.39,1.07 1.71,2.05 0.6,1.82 1.37,2.84 2.73,3.55 0.51,0.23 1.07,0.35 1.64,0.35 2.21,0 4,-1.79 4,-4h-2c0,1.1 -0.9,2 -2,2zM7.64,2.64L6.22,1.22C4.23,3.21 3,5.96 3,9s1.23,5.79 3.22,7.78l1.41,-1.41C6.01,13.74 5,11.49 5,9s1.01,-4.74 2.64,-6.36zM11.5,9c0,1.38 1.12,2.5 2.5,2.5s2.5,-1.12 2.5,-2.5 -1.12,-2.5 -2.5,-2.5 -2.5,1.12 -2.5,2.5z" />
|
||||
</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/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M6.03,3.2C7.15,2.44 8.51,2 10,2c3.93,0 7,3.07 7,7c0,1.26 -0.38,2.65 -1.07,3.9c-0.02,0.04 -0.05,0.08 -0.08,0.13l-1.48,-1.48C14.77,10.69 15,9.8 15,9c0,-2.8 -2.2,-5 -5,-5C9.08,4 8.24,4.26 7.5,4.67L6.03,3.2zM17.21,14.38l1.43,1.43C20.11,13.93 21,11.57 21,9c0,-3.04 -1.23,-5.79 -3.22,-7.78l-1.42,1.42C17.99,4.26 19,6.51 19,9C19,11.02 18.33,12.88 17.21,14.38zM10,6.5c-0.21,0 -0.4,0.03 -0.59,0.08l3.01,3.01C12.47,9.4 12.5,9.21 12.5,9C12.5,7.62 11.38,6.5 10,6.5zM21.19,21.19L2.81,2.81L1.39,4.22l2.13,2.13C3.19,7.16 3,8.05 3,9h2c0,-0.36 0.05,-0.71 0.12,-1.05l6.61,6.61c-0.88,0.68 -1.78,1.41 -2.27,2.9c-0.5,1.5 -1,2.01 -1.71,2.38C7.56,19.94 7.29,20 7,20c-1.1,0 -2,-0.9 -2,-2H3c0,2.21 1.79,4 4,4c0.57,0 1.13,-0.12 1.64,-0.35c1.36,-0.71 2.13,-1.73 2.73,-3.55c0.32,-0.98 0.9,-1.43 1.71,-2.05c0.03,-0.02 0.05,-0.04 0.08,-0.06l6.62,6.62L21.19,21.19z" />
|
||||
</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/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,15c1.66,0 2.99,-1.34 2.99,-3L15,6c0,-1.66 -1.34,-3 -3,-3S9,4.34 9,6v6c0,1.66 1.34,3 3,3zM17.3,12c0,3 -2.54,5.1 -5.3,5.1S6.7,15 6.7,12L5,12c0,3.42 2.72,6.23 6,6.72L11,22h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z" />
|
||||
</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/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM14,17h-2L12,9h-2L10,7h4v10z" />
|
||||
</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/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M16.01,7L16,3h-2v4h-4V3H8v4h-0.01C7,6.99 6,7.99 6,8.99v5.49L9.5,18v3h5v-3l3.5,-3.51v-5.5c0,-1 -1,-2 -1.99,-1.99z" />
|
||||
</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/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12.87,15.07l-2.54,-2.51 0.03,-0.03c1.74,-1.94 2.98,-4.17 3.71,-6.53L17,6L17,4h-7L10,2L8,2v2L1,4v1.99h11.17C11.5,7.92 10.44,9.75 9,11.35 8.07,10.32 7.3,9.19 6.69,8h-2c0.73,1.63 1.73,3.17 2.98,4.56l-5.09,5.02L4,19l5,-5 3.11,3.11 0.76,-2.04zM18.5,10h-2L12,22h2l1.12,-3h4.75L21,22h2l-4.5,-12zM15.88,17l1.62,-4.33L19.12,17h-3.24z" />
|
||||
</vector>
|
||||
@@ -1,8 +1,8 @@
|
||||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
@@ -11,43 +11,97 @@
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pod_left"
|
||||
style="@style/TextAppearance.Compat.Notification.Title"
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:drawableStart="@drawable/ic_airpod_left_24"
|
||||
tools:text="100%" />
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_left_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_airpod_left_24" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pod_left_label"
|
||||
style="@style/TextAppearance.Compat.Notification.Title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
tools:text="100%" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_left_charging"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_baseline_power_24" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_left_ear"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_baseline_hearing_24" />
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pod_case"
|
||||
style="@style/TextAppearance.Compat.Notification.Title"
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:drawableStart="@drawable/ic_airpod_case_24"
|
||||
tools:text="100%" />
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_case_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_airpod_case_24" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pod_case_label"
|
||||
style="@style/TextAppearance.Compat.Notification.Title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
tools:text="100%" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_case_charging"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_baseline_power_24" />
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pod_right"
|
||||
style="@style/TextAppearance.Compat.Notification.Title"
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:drawableStart="@drawable/ic_airpod_right_24"
|
||||
tools:text="100%" />
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_right_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_airpod_right_24" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pod_right_label"
|
||||
style="@style/TextAppearance.Compat.Notification.Title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
tools:text="100%" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_right_charging"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_baseline_power_24" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_right_ear"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_baseline_hearing_24" />
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="0dp"
|
||||
|
||||
@@ -2,7 +2,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:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/headphones_label"
|
||||
@@ -10,16 +11,27 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
tools:text="AirPods Max" />
|
||||
tools:text="Beats Solo 3" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/headphones"
|
||||
style="@style/TextAppearance.Compat.Notification.Title"
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:drawableStart="@drawable/ic_device_generic_headphones"
|
||||
android:gravity="center"
|
||||
tools:text="100%" />
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/headphones_battery_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_baseline_battery_unknown_24" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/headphones_battery_label"
|
||||
style="@style/TextAppearance.Compat.Notification.Title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
tools:text="100%" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -2,7 +2,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:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/headphones_label"
|
||||
@@ -12,14 +13,35 @@
|
||||
android:gravity="center"
|
||||
tools:text="AirPods Max" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/headphones"
|
||||
style="@style/TextAppearance.Compat.Notification.Title"
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:drawableStart="@drawable/ic_device_generic_headphones"
|
||||
android:gravity="center"
|
||||
tools:text="100%" />
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/headphones_battery_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_baseline_battery_unknown_24" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/headphones_battery_label"
|
||||
style="@style/TextAppearance.Compat.Notification.Title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
tools:text="100%" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/headphones_charging"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_baseline_power_24" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/headphones_worn"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_baseline_hearing_24" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
+10
-22
@@ -14,42 +14,30 @@
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/permission_label"
|
||||
android:id="@+id/bluetooth_label"
|
||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/overview_bluetooth_disabled_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:text="@string/overview_nopaireddevices_label" />
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/permission_description"
|
||||
android:id="@+id/bluetooth_description"
|
||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/permission_label"
|
||||
android:text="@string/overview_nopaireddevices_description" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/show_all_action"
|
||||
style="@style/Widget.MaterialComponents.Button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/settings_showall_label"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/overview_bluetooth_disabled_description"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/permission_description" />
|
||||
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/bluetooth_label" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@@ -49,6 +49,18 @@
|
||||
app:layout_constraintTop_toBottomOf="@id/permission_label"
|
||||
tools:text="Lorem Ipsum" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/privacy_policy"
|
||||
style="@style/TextAppearance.MaterialComponents.Tooltip"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/settings_privacy_policy_label"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/permission_description" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/grant_action"
|
||||
style="@style/Widget.MaterialComponents.Button"
|
||||
@@ -60,7 +72,7 @@
|
||||
android:text="@string/general_grant_permission_action"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/permission_description" />
|
||||
app:layout_constraintTop_toBottomOf="@id/privacy_policy" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
@@ -87,61 +87,334 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="top"
|
||||
app:constraint_referenced_ids="pod_left,pod_case,pod_right" />
|
||||
app:constraint_referenced_ids="pod_left_container,pod_case_container,pod_right_container" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/pod_left"
|
||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/pod_left_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:gravity="center_horizontal"
|
||||
app:drawableTopCompat="@drawable/ic_airpod_left_24"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minHeight="108dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
||||
app:layout_constraintEnd_toStartOf="@id/pod_case"
|
||||
app:layout_constraintEnd_toStartOf="@id/pod_case_container"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
tools:text="Left pod\n100%\n(Primary)" />
|
||||
app:layout_constraintVertical_bias="0.0">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/pod_case"
|
||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||
<ImageView
|
||||
android:id="@+id/pod_left_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_airpod_left_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/pod_left_label"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/pod_left_label" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/pod_left_label"
|
||||
style="@style/PodInfoItemText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="@string/pods_dual_left_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/pod_left_icon"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_left_battery_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/pod_left_battery_label"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/pod_left_battery_label" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/pod_left_battery_label"
|
||||
style="@style/PodInfoItemText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@string/general_value_unknown_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/pod_left_battery_icon"
|
||||
app:layout_constraintTop_toBottomOf="@id/pod_left_label" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_left_charging_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_baseline_power_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/pod_left_charging_label"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/pod_left_charging_label" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/pod_left_charging_label"
|
||||
style="@style/PodInfoItemText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@string/pods_charging_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/pod_left_charging_icon"
|
||||
app:layout_constraintTop_toBottomOf="@id/pod_left_battery_label" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_left_microphone_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_baseline_keyboard_voice_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/pod_left_microphone_label"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/pod_left_microphone_label" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/pod_left_microphone_label"
|
||||
style="@style/PodInfoItemText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@string/pods_microphone_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/pod_left_microphone_icon"
|
||||
app:layout_constraintTop_toBottomOf="@id/pod_left_charging_label" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_left_wear_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_baseline_hearing_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/pod_left_wear_label"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/pod_left_wear_label" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/pod_left_wear_label"
|
||||
style="@style/PodInfoItemText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@string/pods_inear_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/pod_left_wear_icon"
|
||||
app:layout_constraintTop_toBottomOf="@id/pod_left_microphone_label" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/pod_case_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:gravity="center_horizontal"
|
||||
app:drawableTopCompat="@drawable/ic_airpod_case_24"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
||||
app:layout_constraintEnd_toStartOf="@id/pod_right"
|
||||
app:layout_constraintStart_toEndOf="@id/pod_left"
|
||||
app:layout_constraintEnd_toStartOf="@id/pod_right_container"
|
||||
app:layout_constraintStart_toEndOf="@id/pod_left_container"
|
||||
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
tools:text="Case\n50%" />
|
||||
app:layout_constraintVertical_bias="0.0">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/pod_right"
|
||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||
<ImageView
|
||||
android:id="@+id/pod_case_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_airpod_case_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/pod_case_label"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/pod_case_label" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/pod_case_label"
|
||||
style="@style/PodInfoItemText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="@string/pods_case_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/pod_case_icon"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_case_battery_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/pod_case_battery_label"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/pod_case_battery_label" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/pod_case_battery_label"
|
||||
style="@style/PodInfoItemText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@string/general_value_unknown_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/pod_case_battery_icon"
|
||||
app:layout_constraintTop_toBottomOf="@id/pod_case_label" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_case_charging_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_baseline_power_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/pod_case_charging_label"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/pod_case_charging_label" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/pod_case_charging_label"
|
||||
style="@style/PodInfoItemText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@string/pods_charging_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/pod_case_charging_icon"
|
||||
app:layout_constraintTop_toBottomOf="@id/pod_case_battery_label" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_case_lid_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_baseline_grid_view_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/pod_case_lid_label"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/pod_case_lid_label" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/pod_case_lid_label"
|
||||
style="@style/PodInfoItemText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@string/pods_case_status_closed_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/pod_case_lid_icon"
|
||||
app:layout_constraintTop_toBottomOf="@id/pod_case_charging_label" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/pod_right_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
app:drawableTopCompat="@drawable/ic_airpod_right_24"
|
||||
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/pod_case"
|
||||
app:layout_constraintStart_toEndOf="@id/pod_case_container"
|
||||
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
tools:text="R 90%" />
|
||||
app:layout_constraintVertical_bias="0.0">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_right_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_airpod_right_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/pod_right_label"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/pod_right_label" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/pod_right_label"
|
||||
style="@style/PodInfoItemText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="@string/pods_dual_right_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/pod_right_icon"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_right_battery_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/pod_right_battery_label"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/pod_right_battery_label" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/pod_right_battery_label"
|
||||
style="@style/PodInfoItemText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@string/general_value_unknown_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/pod_right_battery_icon"
|
||||
app:layout_constraintTop_toBottomOf="@id/pod_right_label" />
|
||||
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_right_charging_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_baseline_power_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/pod_right_charging_label"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/pod_right_charging_label" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/pod_right_charging_label"
|
||||
style="@style/PodInfoItemText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@string/pods_charging_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/pod_right_charging_icon"
|
||||
app:layout_constraintTop_toBottomOf="@id/pod_right_battery_label" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_right_microphone_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_baseline_keyboard_voice_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/pod_right_microphone_label"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/pod_right_microphone_label" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/pod_right_microphone_label"
|
||||
style="@style/PodInfoItemText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@string/pods_microphone_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/pod_right_microphone_icon"
|
||||
app:layout_constraintTop_toBottomOf="@id/pod_right_charging_label" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_right_wear_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_baseline_hearing_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/pod_right_wear_label"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/pod_right_wear_label" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/pod_right_wear_label"
|
||||
style="@style/PodInfoItemText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@string/pods_inear_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/pod_right_wear_icon"
|
||||
app:layout_constraintTop_toBottomOf="@id/pod_right_microphone_label" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier_bottom"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="pod_left,pod_case,pod_right" />
|
||||
app:constraint_referenced_ids="pod_left_container,pod_case_container,pod_right_container" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/status"
|
||||
|
||||
@@ -90,21 +90,40 @@
|
||||
app:constraint_referenced_ids="headphones"
|
||||
tools:layout_editor_absoluteY="60dp" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/headphones"
|
||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
app:drawableTopCompat="@drawable/ic_device_generic_headphones"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_goneMarginBottom="16dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
tools:text="Headphones\n50%\nCharging" />
|
||||
app:layout_goneMarginBottom="16dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/battery_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/battery_label"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/battery_label" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/battery_label"
|
||||
style="@style/PodInfoItemText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@string/general_value_unknown_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/battery_icon"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier_bottom"
|
||||
|
||||
@@ -90,21 +90,81 @@
|
||||
app:constraint_referenced_ids="headphones"
|
||||
tools:layout_editor_absoluteY="60dp" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/headphones"
|
||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
app:drawableTopCompat="@drawable/ic_device_generic_headphones"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:minHeight="64dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_goneMarginBottom="16dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
tools:text="Headphones\n50%\nCharging" />
|
||||
app:layout_goneMarginBottom="16dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/battery_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/battery_label"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/battery_label" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/battery_label"
|
||||
style="@style/PodInfoItemText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@string/general_value_unknown_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/battery_icon"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/charging_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_baseline_power_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/charging_label"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/charging_label" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/charging_label"
|
||||
style="@style/PodInfoItemText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@string/pods_charging_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/charging_icon"
|
||||
app:layout_constraintTop_toBottomOf="@id/battery_label" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/wear_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:src="@drawable/ic_baseline_hearing_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/wear_label"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/wear_label" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/wear_label"
|
||||
style="@style/PodInfoItemText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@string/headset_being_worn_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/wear_icon"
|
||||
app:layout_constraintTop_toBottomOf="@id/charging_label" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier_bottom"
|
||||
|
||||
@@ -1,72 +1,182 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pod_label"
|
||||
style="@style/TextAppearance.Compat.Notification.Title"
|
||||
<ImageView
|
||||
android:id="@+id/pod_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_device_generic_earbuds"
|
||||
app:layout_constraintBottom_toBottomOf="@id/pod_label"
|
||||
app:layout_constraintEnd_toStartOf="@id/pod_label"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/pod_label" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/pod_label"
|
||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
app:layout_constraintEnd_toStartOf="@id/signal"
|
||||
app:layout_constraintStart_toEndOf="@id/pod_icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="AirPods Pro" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/signal"
|
||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:orientation="horizontal">
|
||||
android:drawableEnd="@drawable/ic_baseline_signal_cellular_alt_24"
|
||||
android:gravity="center"
|
||||
android:minWidth="44dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/pod_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/pod_label"
|
||||
tools:text="-90%" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@id/pod_left_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintEnd_toStartOf="@id/pod_case_container"
|
||||
app:layout_constraintHorizontal_chainStyle="spread"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/pod_label">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pod_left"
|
||||
style="@style/TextAppearance.Compat.Notification.Title"
|
||||
<ImageView
|
||||
android:id="@+id/pod_left_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:drawableStart="@drawable/ic_airpod_left_24"
|
||||
android:src="@drawable/ic_airpod_left_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/pod_left_battery_label"
|
||||
app:layout_constraintEnd_toStartOf="@id/pod_left_battery_icon"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/pod_left_battery_label" />
|
||||
|
||||
<ImageView
|
||||
android:id="@id/pod_left_battery_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/pod_left_battery_label"
|
||||
app:layout_constraintEnd_toStartOf="@id/pod_left_battery_label"
|
||||
app:layout_constraintStart_toEndOf="@id/pod_left_icon"
|
||||
app:layout_constraintTop_toTopOf="@id/pod_left_battery_label" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/pod_left_battery_label"
|
||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/pod_left_battery_icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="100%" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pod_case"
|
||||
style="@style/TextAppearance.Compat.Notification.Title"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/pod_case_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintEnd_toStartOf="@id/pod_right_container"
|
||||
app:layout_constraintHorizontal_chainStyle="spread"
|
||||
app:layout_constraintStart_toEndOf="@id/pod_left_container"
|
||||
app:layout_constraintTop_toBottomOf="@id/pod_label">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_case_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:drawableStart="@drawable/ic_airpod_case_24"
|
||||
android:src="@drawable/ic_airpod_case_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/pod_case_battery_label"
|
||||
app:layout_constraintEnd_toStartOf="@id/pod_case_battery_icon"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/pod_case_battery_label" />
|
||||
|
||||
<ImageView
|
||||
android:id="@id/pod_case_battery_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/pod_case_battery_label"
|
||||
app:layout_constraintEnd_toStartOf="@id/pod_case_battery_label"
|
||||
app:layout_constraintStart_toEndOf="@id/pod_case_icon"
|
||||
app:layout_constraintTop_toTopOf="@id/pod_case_battery_label" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/pod_case_battery_label"
|
||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/pod_case_battery_icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="100%" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pod_right"
|
||||
style="@style/TextAppearance.Compat.Notification.Title"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@id/pod_right_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_chainStyle="spread"
|
||||
app:layout_constraintStart_toEndOf="@id/pod_case_container"
|
||||
app:layout_constraintTop_toBottomOf="@id/pod_label">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_right_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:drawableStart="@drawable/ic_airpod_right_24"
|
||||
android:src="@drawable/ic_airpod_right_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/pod_right_battery_label"
|
||||
app:layout_constraintEnd_toStartOf="@id/pod_right_battery_icon"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/pod_right_battery_label" />
|
||||
|
||||
<ImageView
|
||||
android:id="@id/pod_right_battery_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/pod_right_battery_label"
|
||||
app:layout_constraintEnd_toStartOf="@id/pod_right_battery_label"
|
||||
app:layout_constraintStart_toEndOf="@id/pod_right_icon"
|
||||
app:layout_constraintTop_toTopOf="@id/pod_right_battery_label" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/pod_right_battery_label"
|
||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/pod_right_battery_icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="100%" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -1,29 +1,81 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/headphones_label"
|
||||
style="@style/TextAppearance.Compat.Notification.Title"
|
||||
<ImageView
|
||||
android:id="@+id/headphones_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_device_generic_earbuds"
|
||||
app:layout_constraintBottom_toBottomOf="@id/headphones_label"
|
||||
app:layout_constraintEnd_toStartOf="@id/headphones_label"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/headphones_label" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/headphones_label"
|
||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:gravity="center"
|
||||
app:layout_constraintEnd_toStartOf="@id/signal"
|
||||
app:layout_constraintStart_toEndOf="@id/headphones_icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="AirPods Max" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/headphones"
|
||||
style="@style/TextAppearance.Compat.Notification.Title"
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/signal"
|
||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:drawableStart="@drawable/ic_device_generic_headphones"
|
||||
tools:text="100%" />
|
||||
android:drawableEnd="@drawable/ic_baseline_signal_cellular_alt_24"
|
||||
android:gravity="center"
|
||||
android:minWidth="44dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/headphones_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/headphones_label"
|
||||
tools:text="-90%" />
|
||||
|
||||
</LinearLayout>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/headphones_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_chainStyle="spread"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/headphones_label">
|
||||
|
||||
<ImageView
|
||||
android:id="@id/headphones_battery_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/headphones_battery_label"
|
||||
app:layout_constraintEnd_toStartOf="@id/headphones_battery_label"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/headphones_battery_label" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/headphones_battery_label"
|
||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/headphones_battery_icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="100%" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -1,38 +1,81 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
<ImageView
|
||||
android:id="@+id/headphones_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_device_generic_earbuds"
|
||||
app:layout_constraintBottom_toBottomOf="@id/headphones_label"
|
||||
app:layout_constraintEnd_toStartOf="@id/headphones_label"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/headphones_label" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/headphones_label"
|
||||
style="@style/TextAppearance.Compat.Notification.Title"
|
||||
android:layout_width="wrap_content"
|
||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:gravity="center"
|
||||
tools:text="AirPods Max" />
|
||||
app:layout_constraintEnd_toStartOf="@id/signal"
|
||||
app:layout_constraintStart_toEndOf="@id/headphones_icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Beats Solo 3" />
|
||||
|
||||
<LinearLayout
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/signal"
|
||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
android:drawableEnd="@drawable/ic_baseline_signal_cellular_alt_24"
|
||||
android:gravity="center"
|
||||
android:minWidth="44dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/headphones_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/headphones_label"
|
||||
tools:text="-90%" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/headphones"
|
||||
style="@style/TextAppearance.Compat.Notification.Title"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/headphones_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_chainStyle="spread"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/headphones_label">
|
||||
|
||||
<ImageView
|
||||
android:id="@id/headphones_battery_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:drawableStart="@drawable/ic_device_generic_headphones"
|
||||
android:gravity="center"
|
||||
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/headphones_battery_label"
|
||||
app:layout_constraintEnd_toStartOf="@id/headphones_battery_label"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/headphones_battery_label" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/headphones_battery_label"
|
||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/headphones_battery_icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="100%" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -0,0 +1,37 @@
|
||||
<?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"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:minWidth="320dp"
|
||||
android:minHeight="96dp"
|
||||
android:layout_margin="8dp">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/popup_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/close_action"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="1.0" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/close_action"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/general_close_action"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintHorizontal_bias="1.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/popup_content" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@@ -15,6 +15,7 @@
|
||||
<string name="general_grant_permission_action">Grant permission</string>
|
||||
<string name="general_upgrade_action">Upgrade</string>
|
||||
<string name="general_check_action">Check</string>
|
||||
<string name="general_close_action">Close</string>
|
||||
|
||||
<string name="debug_debuglog_size_label">Size</string>
|
||||
<string name="debug_debuglog_size_compressed_label">Compressed size</string>
|
||||
@@ -23,39 +24,23 @@
|
||||
<string name="debug_debuglog_record_action">Record debug log</string>
|
||||
|
||||
<string name="permission_bluetooth_connect_label">Bluetooth connect</string>
|
||||
<string name="permission_bluetooth_connect_description">This permission is required to connect and interact with paired Bluetooth devices.</string>
|
||||
<string name="permission_bluetooth_connect_description">This app requires the \"Bluetooth connect\" permission to interact with paired devices and initiate connections.</string>
|
||||
<string name="permission_bluetooth_scan_label">Bluetooth scanning</string>
|
||||
<string name="permission_bluetooth_scan_description">The Bluetooth scanning permissions allows apps to discover and receive Bluetooth data from nearby devices.</string>
|
||||
<string name="permission_bluetooth_scan_description">The \"Bluetooth scanning\" permission allows this app to discover and receive Bluetooth data from nearby devices such as your AirPods.</string>
|
||||
<string name="permission_bluetooth_label">Bluetooth</string>
|
||||
<string name="permission_bluetooth_description">Allows applications to connect to paired bluetooth devices.</string>
|
||||
<string name="permission_bluetooth_description">This app requires the \"Bluetooth\" to connect to paired bluetooth devices.</string>
|
||||
<string name="permission_access_fine_location_label">Access fine location</string>
|
||||
<string name="permission_access_fine_location_description">The "fine location" permission is required to receive Bluetooth Low Energy data. This app will never access your location! But data from known devices (e.g. AirTags), could be used to determine your location, this is why access to Bluetooth data is guarded by a location permission.</string>
|
||||
<string name="permission_access_fine_location_description">CAPod uses the \"fine location\" permission to receive Bluetooth Low Energy data. Your headphones use Bluetooth Low Energy technology to broadcast their status. This app will NOT use Bluetooth data to determine your location.</string>
|
||||
<string name="permission_background_location_label">Background location access</string>
|
||||
<string name="permission_background_location_description">Background location access is required to receive Bluetooth data while the app is in the background. This app will never access your location! The permission is called "location" because Bluetooth technology can also be used to determine your location.</string>
|
||||
<string name="permission_background_location_description">CAPod uses \"background location access\" to enable features like \"Show popup\" and \"AutoConnect\" while the app is closed. Background location access allows the app to receive Bluetooth Low Energy data while it is in the background. This app will NOT use Bluetooth data to determine your location.</string>
|
||||
<string name="permission_ignore_battery_optimizations_label">Disable battery optimizations</string>
|
||||
<string name="permission_ignore_battery_optimizations_description">Battery optimizations can prevent CAPod from receiving Bluetooth data while the app is in the background.</string>
|
||||
<string name="permission_ignore_battery_optimizations_description">Battery optimizations prevent this app from reliably receiving Bluetooth data while the app is in the background.</string>
|
||||
|
||||
<string name="pods_single_basic_status_short">Headphones: %1$s</string>
|
||||
<string name="pods_single_headphones_label">Headphones</string>
|
||||
<string name="pods_dual_left_label">Left pod</string>
|
||||
<string name="pods_dual_right_label">Right pod</string>
|
||||
<string name="pods_case_label">Case</string>
|
||||
<string name="pods_case_status_open_label">Open</string>
|
||||
<string name="pods_case_status_closed_label">Closed</string>
|
||||
<string name="pods_reception_x_label">Reception %s</string>
|
||||
<string name="pods_device_color_white_label">White</string>
|
||||
<string name="pods_device_color_yellow_label">Yellow</string>
|
||||
<string name="pods_device_color_light_blue_label">Light blue</string>
|
||||
<string name="pods_device_color_dark_blue_label">Dark blue</string>
|
||||
<string name="pods_device_color_space_gray_label">Space gray</string>
|
||||
<string name="pods_device_color_rose_gold_label">Rose gold</string>
|
||||
<string name="pods_device_color_gold_label">Gold</string>
|
||||
<string name="pods_device_color_silver_label">Silver</string>
|
||||
<string name="pods_device_color_gray_label">Gray</string>
|
||||
<string name="pods_device_color_pink_label">Pink</string>
|
||||
<string name="pods_device_color_blue_label">Blue</string>
|
||||
<string name="pods_device_color_red_label">Red</string>
|
||||
<string name="pods_device_color_black_label">Black</string>
|
||||
<string name="pods_connection_state_disconnected_label">Not connected to a device</string>
|
||||
<string name="pods_connection_state_idle_label">Connected to a device, but idle</string>
|
||||
<string name="pods_connection_state_music_label">In music mode</string>
|
||||
@@ -70,10 +55,9 @@
|
||||
<string name="pods_inear_label">In ear</string>
|
||||
<string name="pods_microphone_label">Microphone</string>
|
||||
<string name="pods_yours">Yours</string>
|
||||
<string name="headset_being_worn_label">Being worn</string>
|
||||
|
||||
<string name="overview_nopaireddevices_label">No paired device connected.</string>
|
||||
<string name="overview_nopaireddevices_description">Connect a paired device or enable the \'Show all\' option.</string>
|
||||
<string name="overview_nomaindevice_label">Missing main device</string>
|
||||
<string name="overview_nomaindevice_label">No primary device</string>
|
||||
<string name="overview_nomaindevice_description">All detected devices are unlikely to be yours. Power on and connect your device or adjust the settings.</string>
|
||||
|
||||
<string name="settings_label">Settings</string>
|
||||
@@ -86,7 +70,6 @@
|
||||
<string name="settings_acknowledgements_label">Acknowledgements</string>
|
||||
|
||||
<string name="changelog_label">Changelog</string>
|
||||
<string name="documentation_label">Documentation</string>
|
||||
<string name="settings_support_email_developer_label">Email developer</string>
|
||||
|
||||
<string name="settings_support_installid_label">Install ID</string>
|
||||
@@ -146,4 +129,12 @@
|
||||
<string name="settings_popup_caseopen_label">Show popup</string>
|
||||
<string name="settings_popup_caseopen_description">Show a popup when the device case is opened (experimental).</string>
|
||||
<string name="notification_channel_reaction_popup_label">Popup device reactions</string>
|
||||
<string name="overview_bluetooth_disabled_label">Bluetooth is disabled</string>
|
||||
<string name="overview_bluetooth_disabled_description">Bluetooth is disabled, enable it ;).</string>
|
||||
<string name="help_translate_label">Translation</string>
|
||||
<string name="help_translate_description">Help translate this into your favorite language.</string>
|
||||
<string name="settings_onepod_mode_label">One pod mode</string>
|
||||
<string name="settings_onepod_mode_description">Wearing both pods is not required, wearing a single pod is sufficient to trigger reactions.</string>
|
||||
<string name="permission_system_alert_window_label">System Alert Window</string>
|
||||
<string name="permission_system_alert_window_description">Allow CAPod to draw over other apps to make the feature \"Show PopUp\" possible.</string>
|
||||
</resources>
|
||||
@@ -6,4 +6,14 @@
|
||||
<item name="android:layout_marginStart">8dp</item>
|
||||
<item name="android:layout_marginEnd">8dp</item>
|
||||
</style>
|
||||
|
||||
<style name="PodInfoItemIcon">
|
||||
<item name="android:layout_height">20dp</item>
|
||||
<item name="android:layout_width">20dp</item>
|
||||
</style>
|
||||
|
||||
<style name="PodInfoItemText" parent="TextAppearance.MaterialComponents.Body2">
|
||||
<item name="android:singleLine">true</item>
|
||||
<item name="android:ellipsize">end</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -20,7 +20,6 @@
|
||||
app:title="@string/settings_support_label" />
|
||||
|
||||
<PreferenceCategory android:title="@string/settings_category_other_label">
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/ic_changelog_onsurface"
|
||||
android:key="core.changelog"
|
||||
@@ -28,7 +27,17 @@
|
||||
app:summary="v?.?.?">
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:data="https://github.com/d4rken/capod/releases/latest" />
|
||||
android:data="https://github.com/d4rken/capod-public/releases/latest" />
|
||||
</Preference>
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/ic_baseline_translate_24"
|
||||
android:key="core.translation"
|
||||
android:title="@string/help_translate_label"
|
||||
app:summary="@string/help_translate_description">
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:data="https://crowdin.com/project/capod" />
|
||||
</Preference>
|
||||
|
||||
<Preference
|
||||
@@ -38,13 +47,10 @@
|
||||
app:title="@string/settings_acknowledgements_label" />
|
||||
|
||||
<Preference
|
||||
android:key="core.privacy"
|
||||
android:icon="@drawable/ic_baseline_book_24"
|
||||
app:summary="@string/settings_privacy_policy_desc"
|
||||
app:title="@string/settings_privacy_policy_label">
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:data="https://github.com/d4rken/capod/blob/main/PRIVACY_POLICY.md" />
|
||||
</Preference>
|
||||
app:title="@string/settings_privacy_policy_label" />
|
||||
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
||||
@@ -1,6 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<CheckBoxPreference
|
||||
android:icon="@drawable/ic_baseline_looks_one_24"
|
||||
android:key="reaction.onepod.enabled"
|
||||
android:summary="@string/settings_onepod_mode_description"
|
||||
android:title="@string/settings_onepod_mode_label" />
|
||||
|
||||
|
||||
<CheckBoxPreference
|
||||
android:icon="@drawable/ic_baseline_play_circle_24"
|
||||
android:key="reaction.autoplay.enabled"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
android:title="@string/issue_tracker_label">
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:data="https://github.com/d4rken/capod/issues" />
|
||||
android:data="https://github.com/d4rken/capod-public/issues" />
|
||||
</Preference>
|
||||
<Preference
|
||||
android:icon="@drawable/ic_discord_onsurface"
|
||||
|
||||
@@ -13,7 +13,8 @@ class BasicSingleApplePodsTest : BaseAirPodsTest() {
|
||||
rawDeviceModel shouldBe 0x0520.toUShort()
|
||||
rawStatus shouldBe 0x00.toUByte()
|
||||
rawPodsBattery shouldBe 0xF5.toUByte()
|
||||
rawCaseBattery shouldBe 0x0F.toUByte()
|
||||
rawFlags shouldBe 0x0.toUShort()
|
||||
rawCaseBattery shouldBe 0xF.toUShort()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x01.toUByte()
|
||||
rawSuffix shouldBe 0x00.toUByte()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import eu.darken.capod.pods.core.HasDualPods
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -15,7 +14,8 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
||||
rawDeviceModel shouldBe 0x0e20.toUShort()
|
||||
rawStatus shouldBe 0x54.toUByte()
|
||||
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||
rawCaseBattery shouldBe 0xB5.toUByte()
|
||||
rawFlags shouldBe 0xB.toUShort()
|
||||
rawCaseBattery shouldBe 0x5.toUShort()
|
||||
rawCaseLidState shouldBe 0x31.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x00.toUByte()
|
||||
@@ -27,12 +27,14 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
||||
create<DualApplePods>("07 19 01 0E 20 >2B< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||
// 00101011
|
||||
// --^-----
|
||||
microPhonePod shouldBe HasDualPods.Pod.LEFT
|
||||
isLeftPodMicrophone shouldBe true
|
||||
isRightPodMicrophone shouldBe false
|
||||
}
|
||||
create<DualApplePods>("07 19 01 0E 20 >0B< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||
// 00001011
|
||||
// --^-----
|
||||
microPhonePod shouldBe HasDualPods.Pod.RIGHT
|
||||
isLeftPodMicrophone shouldBe false
|
||||
isRightPodMicrophone shouldBe true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +190,7 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
||||
// Lid just closed
|
||||
create<DualApplePods>("07 19 01 0E 20 55 AA B4 >39< 00 00 08 A6 DB 99 E0 5E 14 85 E5 C2 0B 68 D7 FF C3 A1") {
|
||||
// 39 0011 1001
|
||||
caseLidState shouldBe DualApplePods.LidState.UNKNOWN
|
||||
caseLidState shouldBe DualApplePods.LidState.CLOSED
|
||||
}
|
||||
// Lid closed
|
||||
create<DualApplePods>("07 19 01 0E 20 55 AA B4 38 00 00 F3 F7 08 3B 98 09 C0 DD E4 BD BD 84 55 56 8B 81") {
|
||||
|
||||
@@ -14,7 +14,8 @@ class SingleApplePodsTest : BaseAirPodsTest() {
|
||||
rawDeviceModel shouldBe 0x0A20.toUShort()
|
||||
rawStatus shouldBe 0x62.toUByte()
|
||||
rawPodsBattery shouldBe 0x04.toUByte()
|
||||
rawCaseBattery shouldBe 0x80.toUByte()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0x0.toUShort()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x0F.toUByte()
|
||||
rawSuffix shouldBe 0x40.toUByte()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user