mirror of
https://github.com/nacabaro/vbhelper.git
synced 2026-07-30 00:31:54 +00:00
speeding up HCE
This commit is contained in:
parent
431be5faea
commit
1a83527590
@ -3,6 +3,7 @@ package com.github.nacabaro.vbhelper.screens.scanScreen.converters
|
||||
import androidx.activity.ComponentActivity
|
||||
import com.github.cfogrady.vbnfc.be.BENfcCharacter
|
||||
import com.github.cfogrady.vbnfc.data.NfcCharacter
|
||||
import com.github.cfogrady.vbnfc.vb.SpecialMission
|
||||
import com.github.cfogrady.vbnfc.vb.VBNfcCharacter
|
||||
import com.github.nacabaro.vbhelper.di.VBHelper
|
||||
import com.github.nacabaro.vbhelper.domain.card.Card
|
||||
@ -268,8 +269,8 @@ class FromNfcConverter (
|
||||
SpecialMissions(
|
||||
characterId = characterId,
|
||||
watchId = watchId,
|
||||
missionType = com.github.cfogrady.vbnfc.data.MissionType.NONE,
|
||||
status = com.github.cfogrady.vbnfc.data.MissionStatus.UNAVAILABLE,
|
||||
missionType = SpecialMission.Type.NONE,
|
||||
status = SpecialMission.Status.UNAVAILABLE,
|
||||
goal = 0,
|
||||
progress = 0,
|
||||
timeElapsedInMinutes = 0,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.github.nacabaro.vbhelper.transfer.hce
|
||||
import android.nfc.tech.IsoDep
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import com.github.cfogrady.vitalwear.protos.Character
|
||||
import kotlinx.coroutines.delay
|
||||
@ -8,7 +9,15 @@ import kotlinx.coroutines.runBlocking
|
||||
* Phone-side ISO-DEP client that drives the VitalWear HCE session.
|
||||
* Mirrors the APDU protocol defined in VitalWearHceProtocol on the watch.
|
||||
*/
|
||||
class VitalWearHceReaderClient(private val isoDep: IsoDep) {
|
||||
class VitalWearHceReaderClient(
|
||||
private val isoDep: IsoDep,
|
||||
private val fastMode: Boolean = true,
|
||||
) {
|
||||
companion object {
|
||||
private const val MIN_CHUNK_SIZE = 256
|
||||
private val bestChunkByDevice = linkedMapOf<String, Int>()
|
||||
}
|
||||
|
||||
init {
|
||||
// Import + DB writes can make COMMIT slower than default transceive time on some devices.
|
||||
isoDep.timeout = 10_000
|
||||
@ -32,78 +41,182 @@ class VitalWearHceReaderClient(private val isoDep: IsoDep) {
|
||||
private val STATUS_SUCCESS: Byte = 0x04
|
||||
private val STATUS_FAILURE: Byte = 0x05
|
||||
private val STATUS_SYNCING: Byte = 0x03
|
||||
private val STATUS_ARMED_SEND: Byte = 0x01
|
||||
private val STATUS_ARMED_RECEIVE: Byte = 0x02
|
||||
private val STATUS_IDLE: Byte = 0x00
|
||||
private val CONFIRMATION_MAX_POLLS = 84 // ~21s with adaptive delay schedule
|
||||
private val PREFERRED_MAX_CHUNK_SIZE = 2048
|
||||
// Fast mode is the default; ceremony APDUs are only enabled when fastMode is false.
|
||||
private val ENABLE_TRANSFER_CEREMONY = !fastMode
|
||||
|
||||
private data class ReadPayloadResult(
|
||||
val character: Character,
|
||||
val session: VitalWearHceSessionInfo,
|
||||
)
|
||||
|
||||
private data class HceTransferMetrics(
|
||||
val direction: String,
|
||||
val payloadBytes: Int,
|
||||
val negotiatedChunkBytes: Int,
|
||||
val apduCountTotal: Int,
|
||||
val apduCountData: Int,
|
||||
val statusPollCount: Int,
|
||||
val tSelectNegotiateMs: Long,
|
||||
val tChunkLoopMs: Long,
|
||||
val tCommitAckMs: Long,
|
||||
val tConfirmMs: Long,
|
||||
val tTotalMs: Long,
|
||||
val result: String,
|
||||
) {
|
||||
fun log() {
|
||||
Log.i(
|
||||
"HCE_CLIENT_METRICS",
|
||||
"dir=$direction payloadBytes=$payloadBytes chunk=$negotiatedChunkBytes apduTotal=$apduCountTotal " +
|
||||
"apduData=$apduCountData polls=$statusPollCount selectNegotiateMs=$tSelectNegotiateMs " +
|
||||
"chunkLoopMs=$tChunkLoopMs commitAckMs=$tCommitAckMs confirmMs=$tConfirmMs totalMs=$tTotalMs result=$result"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** MOVE READ: read character bytes first, let caller import/validate, then COMMIT only on success.
|
||||
* If [onCharacterRead] returns false, COMMIT is skipped so source can remain on the watch.
|
||||
*/
|
||||
fun moveCharacterFromWatch(onCharacterRead: (Character) -> Boolean): Boolean {
|
||||
val character = readCharacterFromWatchWithoutCommit()
|
||||
if (!onCharacterRead(character)) {
|
||||
val readResult = readCharacterFromWatchWithoutCommit()
|
||||
if (!onCharacterRead(readResult.character)) {
|
||||
return false
|
||||
}
|
||||
if (!readResult.session.canCommit()) {
|
||||
error("Session does not allow COMMIT for watch->phone transfer")
|
||||
}
|
||||
requireOk(sendApdu(INS_COMMIT, byteArrayOf()), "COMMIT")
|
||||
return true
|
||||
}
|
||||
|
||||
/** READ: watch has called armSend() — phone reads the character off the watch. */
|
||||
fun receiveCharacterFromWatch(): Character {
|
||||
val character = readCharacterFromWatchWithoutCommit()
|
||||
val readResult = readCharacterFromWatchWithoutCommit()
|
||||
if (!readResult.session.canCommit()) {
|
||||
error("Session does not allow COMMIT for watch->phone transfer")
|
||||
}
|
||||
requireOk(sendApdu(INS_COMMIT, byteArrayOf()), "COMMIT")
|
||||
return character
|
||||
return readResult.character
|
||||
}
|
||||
|
||||
private fun readCharacterFromWatchWithoutCommit(): Character {
|
||||
private fun readCharacterFromWatchWithoutCommit(): ReadPayloadResult {
|
||||
val transferStartMs = SystemClock.elapsedRealtime()
|
||||
var apduCount = 0
|
||||
var dataApduCount = 0
|
||||
selectAid()
|
||||
val negResponse = sendApdu(INS_NEGOTIATE, byteArrayOf(MODE_WATCH_TO_PHONE, VERSION))
|
||||
requireOk(negResponse, "NEGOTIATE")
|
||||
val maxChunk = ((negResponse[2].toInt() and 0xFF) shl 8) or (negResponse[3].toInt() and 0xFF)
|
||||
val payloadLen =
|
||||
((negResponse[4].toInt() and 0xFF) shl 24) or
|
||||
((negResponse[5].toInt() and 0xFF) shl 16) or
|
||||
((negResponse[6].toInt() and 0xFF) shl 8) or
|
||||
(negResponse[7].toInt() and 0xFF)
|
||||
Log.d("HCE_CLIENT", "Receiving $payloadLen bytes in chunks of $maxChunk")
|
||||
val payload = ByteArray(payloadLen)
|
||||
apduCount++
|
||||
val session = negotiateSession(MODE_WATCH_TO_PHONE, desiredMaxChunkSize())
|
||||
apduCount++
|
||||
val negotiatedMs = SystemClock.elapsedRealtime()
|
||||
check(session.isWatchToPhone()) { "NEGOTIATE direction mismatch for WATCH_TO_PHONE" }
|
||||
Log.d("HCE_CLIENT", "Receiving ${session.payloadLength} bytes in chunks of ${session.maxChunkSize}")
|
||||
val payload = ByteArray(session.payloadLength)
|
||||
var offset = 0
|
||||
while (offset < payloadLen) {
|
||||
while (offset < session.payloadLength) {
|
||||
val chunkResponse = sendApdu(INS_READ_CHUNK, intToBytes(offset))
|
||||
apduCount++
|
||||
dataApduCount++
|
||||
requireOk(chunkResponse, "READ_CHUNK @ $offset")
|
||||
val chunkData = chunkResponse.dropLast(2).toByteArray()
|
||||
chunkData.copyInto(payload, offset)
|
||||
offset += chunkData.size
|
||||
}
|
||||
return Character.parseFrom(payload)
|
||||
val chunksDoneMs = SystemClock.elapsedRealtime()
|
||||
HceTransferMetrics(
|
||||
direction = "WATCH_TO_PHONE_READ",
|
||||
payloadBytes = payload.size,
|
||||
negotiatedChunkBytes = session.maxChunkSize,
|
||||
apduCountTotal = apduCount,
|
||||
apduCountData = dataApduCount,
|
||||
statusPollCount = 0,
|
||||
tSelectNegotiateMs = negotiatedMs - transferStartMs,
|
||||
tChunkLoopMs = chunksDoneMs - negotiatedMs,
|
||||
tCommitAckMs = 0L,
|
||||
tConfirmMs = 0L,
|
||||
tTotalMs = chunksDoneMs - transferStartMs,
|
||||
result = "read_complete",
|
||||
).log()
|
||||
return ReadPayloadResult(
|
||||
character = Character.parseFrom(payload),
|
||||
session = session,
|
||||
)
|
||||
}
|
||||
|
||||
/** WRITE: watch has called armReceive() — phone pushes a character to the watch. */
|
||||
fun sendCharacterToWatch(character: Character, closeSyncUiAfterCommit: Boolean = true) {
|
||||
val session = sendCharacterToWatchInternal(
|
||||
character = character,
|
||||
closeSyncUiAfterCommit = closeSyncUiAfterCommit,
|
||||
requestedChunkSize = desiredMaxChunkSize(),
|
||||
)
|
||||
rememberBestChunk(session.maxChunkSize)
|
||||
}
|
||||
|
||||
private fun sendCharacterToWatchInternal(
|
||||
character: Character,
|
||||
closeSyncUiAfterCommit: Boolean,
|
||||
requestedChunkSize: Int,
|
||||
): VitalWearHceSessionInfo {
|
||||
val transferStartMs = SystemClock.elapsedRealtime()
|
||||
var apduCount = 0
|
||||
var dataApduCount = 0
|
||||
selectAid()
|
||||
|
||||
// --- True to Toy Ceremony ---
|
||||
// 1. Haptic Lock-on
|
||||
apduCount++
|
||||
if (ENABLE_TRANSFER_CEREMONY) {
|
||||
// Optional toy-like ceremony; kept behind a toggle to avoid transfer overhead.
|
||||
sendApdu(INS_VIBRATE, byteArrayOf())
|
||||
|
||||
// 2. Visual Sync (Start Beam)
|
||||
apduCount++
|
||||
sendApdu(INS_SYNC_UI, byteArrayOf(0x01))
|
||||
apduCount++
|
||||
}
|
||||
|
||||
val payload = character.toByteArray()
|
||||
val negResponse = sendApdu(INS_NEGOTIATE, byteArrayOf(MODE_PHONE_TO_WATCH, VERSION))
|
||||
requireOk(negResponse, "NEGOTIATE")
|
||||
val maxChunk = ((negResponse[2].toInt() and 0xFF) shl 8) or (negResponse[3].toInt() and 0xFF)
|
||||
Log.d("HCE_CLIENT", "Sending ${payload.size} bytes in chunks of $maxChunk")
|
||||
val session = negotiateSession(MODE_PHONE_TO_WATCH, requestedChunkSize)
|
||||
apduCount++
|
||||
val negotiatedMs = SystemClock.elapsedRealtime()
|
||||
check(session.isPhoneToWatch()) { "NEGOTIATE direction mismatch for PHONE_TO_WATCH" }
|
||||
Log.d("HCE_CLIENT", "Sending ${payload.size} bytes in chunks of ${session.maxChunkSize}")
|
||||
var offset = 0
|
||||
while (offset < payload.size) {
|
||||
val chunkEnd = (offset + maxChunk).coerceAtMost(payload.size)
|
||||
val chunkEnd = (offset + session.maxChunkSize).coerceAtMost(payload.size)
|
||||
val chunk = payload.copyOfRange(offset, chunkEnd)
|
||||
requireOk(sendApdu(INS_WRITE_CHUNK, intToBytes(offset) + chunk), "WRITE_CHUNK @ $offset")
|
||||
apduCount++
|
||||
dataApduCount++
|
||||
offset = chunkEnd
|
||||
}
|
||||
requireOk(sendApdu(INS_COMMIT, byteArrayOf()), "COMMIT")
|
||||
|
||||
// 3. Visual Sync (End Beam)
|
||||
if (closeSyncUiAfterCommit) {
|
||||
sendApdu(INS_SYNC_UI, byteArrayOf(0x00))
|
||||
val chunksDoneMs = SystemClock.elapsedRealtime()
|
||||
if (!session.canCommit()) {
|
||||
error("Session does not allow COMMIT for phone->watch transfer")
|
||||
}
|
||||
val commitStartMs = SystemClock.elapsedRealtime()
|
||||
requireOk(sendApdu(INS_COMMIT, byteArrayOf()), "COMMIT")
|
||||
apduCount++
|
||||
val commitDoneMs = SystemClock.elapsedRealtime()
|
||||
|
||||
if (ENABLE_TRANSFER_CEREMONY && closeSyncUiAfterCommit) {
|
||||
sendApdu(INS_SYNC_UI, byteArrayOf(0x00))
|
||||
apduCount++
|
||||
}
|
||||
HceTransferMetrics(
|
||||
direction = "PHONE_TO_WATCH_WRITE",
|
||||
payloadBytes = payload.size,
|
||||
negotiatedChunkBytes = session.maxChunkSize,
|
||||
apduCountTotal = apduCount,
|
||||
apduCountData = dataApduCount,
|
||||
statusPollCount = 0,
|
||||
tSelectNegotiateMs = negotiatedMs - transferStartMs,
|
||||
tChunkLoopMs = chunksDoneMs - negotiatedMs,
|
||||
tCommitAckMs = commitDoneMs - commitStartMs,
|
||||
tConfirmMs = 0L,
|
||||
tTotalMs = commitDoneMs - transferStartMs,
|
||||
result = "commit_acked",
|
||||
).log()
|
||||
return session
|
||||
}
|
||||
|
||||
/**
|
||||
@ -111,28 +224,87 @@ class VitalWearHceReaderClient(private val isoDep: IsoDep) {
|
||||
* Returns true only when the watch reports import success.
|
||||
*/
|
||||
fun sendCharacterToWatchAndConfirm(character: Character): Boolean {
|
||||
sendCharacterToWatch(character, closeSyncUiAfterCommit = false)
|
||||
var lastError: Exception? = null
|
||||
for (candidateChunk in chunkCandidates()) {
|
||||
val confirmStartMs = SystemClock.elapsedRealtime()
|
||||
val session = try {
|
||||
sendCharacterToWatchInternal(
|
||||
character = character,
|
||||
closeSyncUiAfterCommit = false,
|
||||
requestedChunkSize = candidateChunk,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
lastError = e
|
||||
Log.w("HCE_CLIENT", "Chunk attempt failed before confirmation at chunk=$candidateChunk", e)
|
||||
continue
|
||||
}
|
||||
if (!session.canPollStatus()) {
|
||||
return false
|
||||
}
|
||||
var polls = 0
|
||||
var apduCount = 0
|
||||
var result = "timeout"
|
||||
val confirmed = runBlocking {
|
||||
repeat(12) {
|
||||
repeat(CONFIRMATION_MAX_POLLS) {
|
||||
val statusResponse = sendApdu(INS_STATUS, byteArrayOf())
|
||||
apduCount++
|
||||
polls++
|
||||
requireOk(statusResponse, "STATUS")
|
||||
val statusByte = statusResponse.dropLast(2).firstOrNull()
|
||||
Log.d("HCE_CLIENT", "STATUS poll[$it]: statusByte=${statusByte?.toInt()?.and(0xFF)}")
|
||||
if (statusByte == STATUS_SUCCESS) {
|
||||
result = "success"
|
||||
return@runBlocking true
|
||||
}
|
||||
if (statusByte == STATUS_FAILURE) {
|
||||
result = "failure"
|
||||
return@runBlocking false
|
||||
}
|
||||
delay(250)
|
||||
if (
|
||||
statusByte == STATUS_SYNCING ||
|
||||
statusByte == STATUS_ARMED_RECEIVE ||
|
||||
statusByte == STATUS_ARMED_SEND ||
|
||||
statusByte == STATUS_IDLE
|
||||
) {
|
||||
delay(delayForPoll(it))
|
||||
return@repeat
|
||||
}
|
||||
// Unknown status byte: keep waiting briefly instead of failing early.
|
||||
delay(delayForPoll(it))
|
||||
}
|
||||
false
|
||||
}
|
||||
if (ENABLE_TRANSFER_CEREMONY) {
|
||||
// End the sync UI after we have consumed terminal status.
|
||||
runCatching {
|
||||
sendApdu(INS_SYNC_UI, byteArrayOf(0x00))
|
||||
}
|
||||
return confirmed
|
||||
}
|
||||
HceTransferMetrics(
|
||||
direction = "PHONE_TO_WATCH_CONFIRM",
|
||||
payloadBytes = character.serializedSize,
|
||||
negotiatedChunkBytes = session.maxChunkSize,
|
||||
apduCountTotal = apduCount,
|
||||
apduCountData = 0,
|
||||
statusPollCount = polls,
|
||||
tSelectNegotiateMs = 0L,
|
||||
tChunkLoopMs = 0L,
|
||||
tCommitAckMs = 0L,
|
||||
tConfirmMs = SystemClock.elapsedRealtime() - confirmStartMs,
|
||||
tTotalMs = SystemClock.elapsedRealtime() - confirmStartMs,
|
||||
result = "$result(requested=$candidateChunk)",
|
||||
).log()
|
||||
if (confirmed) {
|
||||
rememberBestChunk(session.maxChunkSize)
|
||||
return true
|
||||
}
|
||||
// Explicit failure means import rejected payload; retrying chunk size won't help.
|
||||
if (result == "failure") {
|
||||
return false
|
||||
}
|
||||
}
|
||||
lastError?.let { throw it }
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
@ -141,9 +313,8 @@ class VitalWearHceReaderClient(private val isoDep: IsoDep) {
|
||||
*/
|
||||
fun verifyWatchReadyToReceive(): Boolean {
|
||||
selectAid()
|
||||
val negResponse = sendApdu(INS_NEGOTIATE, byteArrayOf(MODE_PHONE_TO_WATCH, VERSION))
|
||||
requireOk(negResponse, "NEGOTIATE")
|
||||
return true
|
||||
val session = negotiateSession(MODE_PHONE_TO_WATCH, desiredMaxChunkSize())
|
||||
return session.canCommit()
|
||||
}
|
||||
|
||||
private fun selectAid() {
|
||||
@ -152,9 +323,102 @@ class VitalWearHceReaderClient(private val isoDep: IsoDep) {
|
||||
if (statusWord(response) != SW_OK) error("SELECT AID failed: ${response.toHex()}")
|
||||
}
|
||||
private fun sendApdu(ins: Byte, data: ByteArray): ByteArray {
|
||||
val apdu = byteArrayOf(CLA, ins, 0x00, 0x00, data.size.toByte()) + data
|
||||
val apdu = if (data.size <= 0xFF) {
|
||||
byteArrayOf(CLA, ins, 0x00, 0x00, data.size.toByte()) + data
|
||||
} else {
|
||||
byteArrayOf(
|
||||
CLA,
|
||||
ins,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
((data.size ushr 8) and 0xFF).toByte(),
|
||||
(data.size and 0xFF).toByte(),
|
||||
) + data
|
||||
}
|
||||
return isoDep.transceive(apdu)
|
||||
}
|
||||
|
||||
private fun negotiateSession(mode: Byte, requestedChunkSize: Int): VitalWearHceSessionInfo {
|
||||
val negResponse = sendApdu(
|
||||
INS_NEGOTIATE,
|
||||
byteArrayOf(mode, VERSION) + intToShortBytes(requestedChunkSize)
|
||||
)
|
||||
requireOk(negResponse, "NEGOTIATE")
|
||||
val negotiatedMode = when (negResponse[1]) {
|
||||
MODE_WATCH_TO_PHONE -> VitalWearHceTransferDirection.WATCH_TO_PHONE
|
||||
MODE_PHONE_TO_WATCH -> VitalWearHceTransferDirection.PHONE_TO_WATCH
|
||||
else -> error("NEGOTIATE returned unknown direction: ${negResponse[1]}")
|
||||
}
|
||||
val maxChunk = ((negResponse[2].toInt() and 0xFF) shl 8) or (negResponse[3].toInt() and 0xFF)
|
||||
val payloadLen =
|
||||
((negResponse[4].toInt() and 0xFF) shl 24) or
|
||||
((negResponse[5].toInt() and 0xFF) shl 16) or
|
||||
((negResponse[6].toInt() and 0xFF) shl 8) or
|
||||
(negResponse[7].toInt() and 0xFF)
|
||||
return VitalWearHceSession(
|
||||
direction = negotiatedMode,
|
||||
maxChunkSize = maxChunk,
|
||||
payloadLength = payloadLen,
|
||||
)
|
||||
}
|
||||
|
||||
private fun desiredMaxChunkSize(): Int {
|
||||
val transceiveLimit = runCatching { isoDep.maxTransceiveLength }.getOrNull()
|
||||
val maxByDevice = if (transceiveLimit != null && transceiveLimit > 0) {
|
||||
// Extended APDU write overhead: 7-byte header + 4-byte offset.
|
||||
(transceiveLimit - 11).coerceAtLeast(1)
|
||||
} else {
|
||||
PREFERRED_MAX_CHUNK_SIZE
|
||||
}
|
||||
val cappedByDevice = maxByDevice.coerceAtMost(PREFERRED_MAX_CHUNK_SIZE)
|
||||
val cachedBest = cachedBestChunk()
|
||||
return if (cachedBest == null) {
|
||||
cappedByDevice
|
||||
} else {
|
||||
cachedBest.coerceIn(MIN_CHUNK_SIZE, cappedByDevice)
|
||||
}
|
||||
}
|
||||
|
||||
private fun chunkCandidates(): List<Int> {
|
||||
val maxChunk = desiredMaxChunkSize()
|
||||
val baselineCandidates = listOf(maxChunk, 2048, 1536, 1024, 768, 512, MIN_CHUNK_SIZE)
|
||||
return baselineCandidates
|
||||
.map { it.coerceAtMost(maxChunk) }
|
||||
.filter { it in MIN_CHUNK_SIZE..maxChunk }
|
||||
.distinct()
|
||||
}
|
||||
|
||||
private fun deviceKey(): String {
|
||||
val tagId = isoDep.tag?.id ?: return "unknown-device"
|
||||
return tagId.toHex()
|
||||
}
|
||||
|
||||
private fun cachedBestChunk(): Int? {
|
||||
synchronized(bestChunkByDevice) {
|
||||
return bestChunkByDevice[deviceKey()]
|
||||
}
|
||||
}
|
||||
|
||||
private fun rememberBestChunk(chunkSize: Int) {
|
||||
synchronized(bestChunkByDevice) {
|
||||
bestChunkByDevice[deviceKey()] = chunkSize
|
||||
}
|
||||
}
|
||||
|
||||
private fun delayForPoll(pollIndex: Int): Long {
|
||||
return when {
|
||||
pollIndex < 20 -> 80L
|
||||
pollIndex < 56 -> 175L
|
||||
else -> 300L
|
||||
}
|
||||
}
|
||||
|
||||
private fun intToShortBytes(value: Int): ByteArray = byteArrayOf(
|
||||
((value ushr 8) and 0xFF).toByte(),
|
||||
(value and 0xFF).toByte()
|
||||
)
|
||||
|
||||
private fun requireOk(response: ByteArray, context: String) {
|
||||
if (statusWord(response) != SW_OK) error("$context failed: ${response.toHex()}")
|
||||
}
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
package com.github.nacabaro.vbhelper.transfer.hce
|
||||
|
||||
internal enum class VitalWearHceTransferDirection {
|
||||
WATCH_TO_PHONE,
|
||||
PHONE_TO_WATCH,
|
||||
}
|
||||
|
||||
internal interface VitalWearHceSessionInfo {
|
||||
val direction: VitalWearHceTransferDirection
|
||||
val maxChunkSize: Int
|
||||
val payloadLength: Int
|
||||
|
||||
fun isWatchToPhone(): Boolean {
|
||||
return direction == VitalWearHceTransferDirection.WATCH_TO_PHONE
|
||||
}
|
||||
|
||||
fun isPhoneToWatch(): Boolean {
|
||||
return direction == VitalWearHceTransferDirection.PHONE_TO_WATCH
|
||||
}
|
||||
|
||||
fun canCommit(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
fun canPollStatus(): Boolean {
|
||||
// Import confirmation polling only applies for phone->watch transfers.
|
||||
return isPhoneToWatch()
|
||||
}
|
||||
}
|
||||
|
||||
internal data class VitalWearHceSession(
|
||||
override val direction: VitalWearHceTransferDirection,
|
||||
override val maxChunkSize: Int,
|
||||
override val payloadLength: Int,
|
||||
) : VitalWearHceSessionInfo
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user