diff --git a/play-services-nearby/build.gradle b/play-services-nearby/build.gradle index dfd0bf02ce..2607f4a22c 100644 --- a/play-services-nearby/build.gradle +++ b/play-services-nearby/build.gradle @@ -27,6 +27,10 @@ android { sourceCompatibility = 1.8 targetCompatibility = 1.8 } + + testOptions { + unitTests.returnDefaultValues = true + } } apply from: '../gradle/publish-android.gradle' @@ -40,4 +44,5 @@ dependencies { api project(':play-services-base') api project(':play-services-basement') api project(':play-services-tasks') + testImplementation 'junit:junit:4.13.2' } diff --git a/play-services-nearby/core/build.gradle b/play-services-nearby/core/build.gradle index 516a65316a..de46b9be90 100644 --- a/play-services-nearby/core/build.gradle +++ b/play-services-nearby/core/build.gradle @@ -61,6 +61,10 @@ android { targetCompatibility = 1.8 } + testOptions { + unitTests.returnDefaultValues = true + } + kotlinOptions { jvmTarget = 1.8 } diff --git a/play-services-nearby/core/package/src/main/AndroidManifest.xml b/play-services-nearby/core/package/src/main/AndroidManifest.xml index 4a226e637e..0a12679be7 100644 --- a/play-services-nearby/core/package/src/main/AndroidManifest.xml +++ b/play-services-nearby/core/package/src/main/AndroidManifest.xml @@ -4,6 +4,13 @@ --> + + + + + + + diff --git a/play-services-nearby/core/src/main/AndroidManifest.xml b/play-services-nearby/core/src/main/AndroidManifest.xml index 9f215f0125..421964ea42 100644 --- a/play-services-nearby/core/src/main/AndroidManifest.xml +++ b/play-services-nearby/core/src/main/AndroidManifest.xml @@ -4,10 +4,23 @@ --> + + + + + + + + + + + + + + - @@ -75,5 +88,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbyCrypto.kt b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbyCrypto.kt new file mode 100644 index 0000000000..3c428f972b --- /dev/null +++ b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbyCrypto.kt @@ -0,0 +1,291 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.nearby.sharing + +import com.google.security.cryptauth.lib.securegcm.DeviceToDeviceMessage +import com.google.security.cryptauth.lib.securegcm.GcmMetadata +import com.google.security.cryptauth.lib.securegcm.Type +import com.google.security.cryptauth.lib.securemessage.EcP256PublicKey +import com.google.security.cryptauth.lib.securemessage.EncScheme +import com.google.security.cryptauth.lib.securemessage.GenericPublicKey +import com.google.security.cryptauth.lib.securemessage.Header +import com.google.security.cryptauth.lib.securemessage.HeaderAndBody +import com.google.security.cryptauth.lib.securemessage.PublicKeyType +import com.google.security.cryptauth.lib.securemessage.SecureMessage +import com.google.security.cryptauth.lib.securemessage.SigScheme +import okio.ByteString +import okio.ByteString.Companion.toByteString +import java.math.BigInteger +import java.nio.charset.StandardCharsets +import java.security.AlgorithmParameters +import java.security.KeyFactory +import java.security.KeyPair +import java.security.KeyPairGenerator +import java.security.MessageDigest +import java.security.PublicKey +import java.security.SecureRandom +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.ECPublicKey +import java.security.spec.ECGenParameterSpec +import java.security.spec.ECParameterSpec +import java.security.spec.ECPoint +import java.security.spec.ECPublicKeySpec +import java.util.Locale +import javax.crypto.Cipher +import javax.crypto.KeyAgreement +import javax.crypto.Mac +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +class NearbyCrypto(val isServer: Boolean) { + val keyPair: KeyPair = generateECKeyPair() + val random: SecureRandom = SecureRandom() + + var decryptKey: ByteArray? = null + var encryptKey: ByteArray? = null + var recvHmacKey: ByteArray? = null + var sendHmacKey: ByteArray? = null + + var authKey: ByteArray? = null + var pinCode: String = "" + + private var sendSeq: Int = 0 + private var recvSeq: Int = 0 + + fun getGenericPublicKey(): GenericPublicKey { + val ecPub = keyPair.public as ECPublicKey + val x = bigIntegerTo32Bytes(ecPub.w.affineX) + val y = bigIntegerTo32Bytes(ecPub.w.affineY) + return GenericPublicKey( + type = PublicKeyType.EC_P256, + ec_p256_public_key = EcP256PublicKey(x = x, y = y) + ) + } + + fun completeKeyExchange( + peerGenericKey: GenericPublicKey, + clientInitBytes: ByteArray, + serverInitBytes: ByteArray + ) { + val ecP256 = peerGenericKey.ec_p256_public_key + ?: throw IllegalArgumentException("Missing ec_p256_public_key") + val peerPublicKey = decodeECPublicKey(ecP256.x.toByteArray(), ecP256.y.toByteArray()) + + val ka = KeyAgreement.getInstance("ECDH") + ka.init(keyPair.private) + ka.doPhase(peerPublicKey, true) + val sharedSecret = ka.generateSecret() + + val sha = MessageDigest.getInstance("SHA-256") + val derivedSecret = sha.digest(sharedSecret) + + val ukeyInfo = ByteArray(clientInitBytes.size + serverInitBytes.size) + System.arraycopy(clientInitBytes, 0, ukeyInfo, 0, clientInitBytes.size) + System.arraycopy(serverInitBytes, 0, ukeyInfo, clientInitBytes.size, serverInitBytes.size) + + val authString = hkdf( + ikm = derivedSecret, + salt = "UKEY2 v1 auth".toByteArray(StandardCharsets.UTF_8), + info = ukeyInfo, + length = 32 + ) + val nextSecret = hkdf( + ikm = derivedSecret, + salt = "UKEY2 v1 next".toByteArray(StandardCharsets.UTF_8), + info = ukeyInfo, + length = 32 + ) + + authKey = authString + pinCode = pinCodeFromAuthKey(authString) + + val d2dClientKey = hkdf( + ikm = nextSecret, + salt = NearbySharingConstants.D2D_SALT, + info = "client".toByteArray(StandardCharsets.UTF_8), + length = 32 + ) + val d2dServerKey = hkdf( + ikm = nextSecret, + salt = NearbySharingConstants.D2D_SALT, + info = "server".toByteArray(StandardCharsets.UTF_8), + length = 32 + ) + + val smsgSalt = sha.digest("SecureMessage".toByteArray(StandardCharsets.UTF_8)) + + val clientKey = hkdf(d2dClientKey, smsgSalt, "ENC:2".toByteArray(StandardCharsets.UTF_8), 32) + val clientHmacKey = hkdf(d2dClientKey, smsgSalt, "SIG:1".toByteArray(StandardCharsets.UTF_8), 32) + val serverKey = hkdf(d2dServerKey, smsgSalt, "ENC:2".toByteArray(StandardCharsets.UTF_8), 32) + val serverHmacKey = hkdf(d2dServerKey, smsgSalt, "SIG:1".toByteArray(StandardCharsets.UTF_8), 32) + + if (isServer) { + decryptKey = clientKey + recvHmacKey = clientHmacKey + encryptKey = serverKey + sendHmacKey = serverHmacKey + } else { + decryptKey = serverKey + recvHmacKey = serverHmacKey + encryptKey = clientKey + sendHmacKey = clientHmacKey + } + } + + fun encryptOfflineFrame(frameBytes: ByteArray): ByteArray { + val encKey = encryptKey ?: throw IllegalStateException("Keys not initialized") + val hmacKey = sendHmacKey ?: throw IllegalStateException("Keys not initialized") + + sendSeq++ + val d2d = DeviceToDeviceMessage( + sequence_number = sendSeq, + message = frameBytes.toByteString() + ) + val d2dBytes = d2d.encode() + + val iv = ByteArray(16).apply { random.nextBytes(this) } + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(encKey, "AES"), IvParameterSpec(iv)) + val encryptedBody = cipher.doFinal(d2dBytes) + + val metadata = GcmMetadata( + type = Type.DEVICE_TO_DEVICE_MESSAGE, + version = 1 + ) + + val header = Header( + encryption_scheme = EncScheme.AES_256_CBC, + signature_scheme = SigScheme.HMAC_SHA256, + iv = iv.toByteString(), + public_metadata = metadata.encode().toByteString() + ) + + val headerAndBody = HeaderAndBody( + header_ = header, + body = encryptedBody.toByteString() + ) + val hbBytes = headerAndBody.encode() + + val mac = Mac.getInstance("HmacSHA256") + mac.init(SecretKeySpec(hmacKey, "HmacSHA256")) + val signature = mac.doFinal(hbBytes) + + val smsg = SecureMessage( + header_and_body = hbBytes.toByteString(), + signature = signature.toByteString() + ) + return smsg.encode() + } + + fun decryptSecureMessage(secureMessageBytes: ByteArray): ByteArray { + val decKey = decryptKey ?: throw IllegalStateException("Keys not initialized") + val hmacKey = recvHmacKey ?: throw IllegalStateException("Keys not initialized") + + val smsg = SecureMessage.ADAPTER.decode(secureMessageBytes) + val hbBytes = smsg.header_and_body.toByteArray() + val signature = smsg.signature.toByteArray() + + val mac = Mac.getInstance("HmacSHA256") + mac.init(SecretKeySpec(hmacKey, "HmacSHA256")) + val expectedSig = mac.doFinal(hbBytes) + if (!MessageDigest.isEqual(expectedSig, signature)) { + throw SecurityException("HMAC signature mismatch") + } + + val headerAndBody = HeaderAndBody.ADAPTER.decode(hbBytes) + val iv = headerAndBody.header_.iv?.toByteArray() + ?: throw IllegalArgumentException("Missing IV in Header") + val cipherText = headerAndBody.body.toByteArray() + + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(decKey, "AES"), IvParameterSpec(iv)) + val decryptedD2D = cipher.doFinal(cipherText) + + val d2d = DeviceToDeviceMessage.ADAPTER.decode(decryptedD2D) + recvSeq++ + if (d2d.sequence_number != recvSeq) { + throw SecurityException("Invalid sequence number: expected $recvSeq got ${d2d.sequence_number}") + } + + return d2d.message?.toByteArray() ?: ByteArray(0) + } + + companion object { + fun generateECKeyPair(): KeyPair { + val kpg = KeyPairGenerator.getInstance("EC") + kpg.initialize(ECGenParameterSpec("secp256r1")) + return kpg.generateKeyPair() + } + + fun bigIntegerTo32Bytes(b: BigInteger): ByteString { + val bytes = b.toByteArray() + val result = ByteArray(32) + if (bytes.size >= 32) { + System.arraycopy(bytes, bytes.size - 32, result, 0, 32) + } else { + System.arraycopy(bytes, 0, result, 32 - bytes.size, bytes.size) + } + return result.toByteString() + } + + fun decodeECPublicKey(xBytes: ByteArray, yBytes: ByteArray): PublicKey { + val x = BigInteger(1, xBytes) + val y = BigInteger(1, yBytes) + val point = ECPoint(x, y) + + val params = AlgorithmParameters.getInstance("EC") + params.init(ECGenParameterSpec("secp256r1")) + val ecSpec = params.getParameterSpec(ECParameterSpec::class.java) + + val kf = KeyFactory.getInstance("EC") + return kf.generatePublic(ECPublicKeySpec(point, ecSpec)) + } + + fun hkdf(ikm: ByteArray, salt: ByteArray, info: ByteArray, length: Int): ByteArray { + val prk = hkdfExtract(salt, ikm) + return hkdfExpand(prk, info, length) + } + + fun hkdfExtract(salt: ByteArray, ikm: ByteArray): ByteArray { + val mac = Mac.getInstance("HmacSHA256") + val key = if (salt.isEmpty()) ByteArray(32) else salt + mac.init(SecretKeySpec(key, "HmacSHA256")) + return mac.doFinal(ikm) + } + + fun hkdfExpand(prk: ByteArray, info: ByteArray, length: Int): ByteArray { + val mac = Mac.getInstance("HmacSHA256") + mac.init(SecretKeySpec(prk, "HmacSHA256")) + val okm = ByteArray(length) + var t = ByteArray(0) + var offset = 0 + var i = 1 + while (offset < length) { + mac.reset() + mac.update(t) + mac.update(info) + mac.update(i.toByte()) + t = mac.doFinal() + val chunkLen = Math.min(t.size, length - offset) + System.arraycopy(t, 0, okm, offset, chunkLen) + offset += chunkLen + i++ + } + return okm + } + + fun pinCodeFromAuthKey(authKey: ByteArray): String { + var hash = 0 + var multiplier = 1 + for (b in authKey) { + val byteVal = b.toInt() + hash = (hash + byteVal * multiplier) % 9973 + multiplier = (multiplier * 31) % 9973 + } + return String.format(Locale.US, "%04d", Math.abs(hash)) + } + } +} diff --git a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbyDevice.kt b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbyDevice.kt new file mode 100644 index 0000000000..2b5ae21135 --- /dev/null +++ b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbyDevice.kt @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.nearby.sharing + +import java.net.InetAddress + +enum class NearbyDeviceType(val value: Int) { + UNKNOWN(0), + PHONE(1), + TABLET(2), + COMPUTER(3); + + companion object { + fun fromValue(value: Int): NearbyDeviceType = when (value) { + 1 -> PHONE + 2 -> TABLET + 3 -> COMPUTER + else -> UNKNOWN + } + } +} + +data class NearbyDevice( + val id: String, + val name: String, + val type: NearbyDeviceType = NearbyDeviceType.PHONE, + val host: InetAddress? = null, + val port: Int = 0, + val endpointId: String? = null, + val endpointInfo: ByteArray? = null +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is NearbyDevice) return false + return id == other.id + } + + override fun hashCode(): Int = id.hashCode() +} diff --git a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbyEndpointInfo.kt b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbyEndpointInfo.kt new file mode 100644 index 0000000000..6015e11a98 --- /dev/null +++ b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbyEndpointInfo.kt @@ -0,0 +1,76 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.nearby.sharing + +import okio.ByteString.Companion.decodeBase64 +import okio.ByteString.Companion.toByteString +import java.security.SecureRandom + +class NearbyEndpointInfo( + val name: String, + val deviceType: NearbyDeviceType = NearbyDeviceType.PHONE, + val isVisible: Boolean = true, + val salt: ByteArray = ByteArray(16).apply { SecureRandom().nextBytes(this) } +) { + fun serialize(): ByteArray { + val nameBytes = name.toByteArray(Charsets.UTF_8).let { + if (it.size > 255) it.copyOf(255) else it + } + + // Byte 0: version (0) | visibility bit | device type bits + val visibilityBit = if (isVisible) 0 else 0x10 + val typeBits = (deviceType.value and 0x07) shl 1 + val byte0 = (visibilityBit or typeBits).toByte() + + val output = ByteArray(1 + 16 + (if (isVisible) 1 + nameBytes.size else 0)) + output[0] = byte0 + System.arraycopy(salt, 0, output, 1, 16) + + if (isVisible) { + output[17] = nameBytes.size.toByte() + System.arraycopy(nameBytes, 0, output, 18, nameBytes.size) + } + + return output + } + + fun toBase64(): String = serialize().toByteString().base64Url().replace("=", "") + companion object { + fun parse(data: ByteArray): NearbyEndpointInfo? { + if (data.size < 17) return null + val byte0 = data[0].toInt() and 0xFF + val isVisible = (byte0 and 0x10) == 0 + val rawType = (byte0 and 0x0E) shr 1 + val deviceType = NearbyDeviceType.fromValue(rawType) + + val salt = ByteArray(16) + System.arraycopy(data, 1, salt, 0, 16) + + val name = if (isVisible && data.size >= 18) { + val nameLength = data[17].toInt() and 0xFF + if (data.size >= 18 + nameLength) { + String(data, 18, nameLength, Charsets.UTF_8) + } else { + "Nearby Device" + } + } else { + "Nearby Device" + } + + return NearbyEndpointInfo(name, deviceType, isVisible, salt) + } + + fun parseBase64(base64: String): NearbyEndpointInfo? { + return try { + val decoded = base64.decodeBase64()?.toByteArray() + ?: base64.replace("-", "+").replace("_", "/").decodeBase64()?.toByteArray() + if (decoded != null) parse(decoded) else null + } catch (e: Exception) { + null + } + } + } +} diff --git a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbySharingAdvertiser.kt b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbySharingAdvertiser.kt new file mode 100644 index 0000000000..6ad2c83bc7 --- /dev/null +++ b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbySharingAdvertiser.kt @@ -0,0 +1,105 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.nearby.sharing +import android.annotation.TargetApi +import android.content.Context +import android.net.nsd.NsdManager +import android.net.nsd.NsdServiceInfo +import android.os.Build +import okio.ByteString.Companion.toByteString +import android.util.Log +import java.security.SecureRandom + +@TargetApi(21) +class NearbySharingAdvertiser( + private val context: Context, + private val deviceName: String, + private val deviceType: NearbyDeviceType = NearbyDeviceType.PHONE +) { + private val nsdManager = context.getSystemService(Context.NSD_SERVICE) as? NsdManager + private var registrationListener: NsdManager.RegistrationListener? = null + val endpointId: String = generateEndpointId() + + private fun generateEndpointId(): String { + val alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + val random = SecureRandom() + val sb = StringBuilder(4) + for (i in 0 until 4) { + sb.append(alphabet[random.nextInt(alphabet.length)]) + } + return sb.toString() + } + + private fun buildServiceName(): String { + val bytes = ByteArray(10) + bytes[0] = NearbySharingConstants.PCP_BYTE + val epBytes = endpointId.toByteArray(Charsets.US_ASCII) + System.arraycopy(epBytes, 0, bytes, 1, Math.min(epBytes.size, 4)) + System.arraycopy(NearbySharingConstants.SERVICE_ID_HASH, 0, bytes, 5, 3) + bytes[8] = 0 + bytes[9] = 0 + return bytes.toByteString().base64Url().replace("=", "") + } + + fun startAdvertising(port: Int, onStarted: (() -> Unit)? = null, onError: ((Int) -> Unit)? = null) { + if (Build.VERSION.SDK_INT < 21) { + Log.w(NearbySharingConstants.TAG, "Nearby Sharing requires API 21+") + onError?.invoke(-1) + return + } + if (nsdManager == null) { + Log.w(NearbySharingConstants.TAG, "NsdManager not available") + onError?.invoke(-1) + return + } + + val serviceInfo = NsdServiceInfo().apply { + serviceType = NearbySharingConstants.SERVICE_TYPE + serviceName = buildServiceName() + setPort(port) + val info = NearbyEndpointInfo(deviceName, deviceType) + setAttribute("n", info.toBase64()) + } + + registrationListener = object : NsdManager.RegistrationListener { + override fun onServiceRegistered(registeredInfo: NsdServiceInfo) { + Log.d(NearbySharingConstants.TAG, "mDNS service registered: ${registeredInfo.serviceName} on port $port") + onStarted?.invoke() + } + + override fun onRegistrationFailed(info: NsdServiceInfo, errorCode: Int) { + Log.e(NearbySharingConstants.TAG, "mDNS registration failed: $errorCode") + onError?.invoke(errorCode) + } + + override fun onServiceUnregistered(info: NsdServiceInfo) { + Log.d(NearbySharingConstants.TAG, "mDNS service unregistered") + } + + override fun onUnregistrationFailed(info: NsdServiceInfo, errorCode: Int) { + Log.e(NearbySharingConstants.TAG, "mDNS unregistration failed: $errorCode") + } + } + + try { + nsdManager.registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, registrationListener) + } catch (e: Exception) { + Log.e(NearbySharingConstants.TAG, "Exception during registerService", e) + onError?.invoke(-1) + } + } + + fun stopAdvertising() { + registrationListener?.let { + try { + nsdManager?.unregisterService(it) + } catch (e: Exception) { + Log.e(NearbySharingConstants.TAG, "Error unregistering service", e) + } + registrationListener = null + } + } +} diff --git a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbySharingClient.kt b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbySharingClient.kt new file mode 100644 index 0000000000..8bf0b2f0d0 --- /dev/null +++ b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbySharingClient.kt @@ -0,0 +1,354 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.nearby.sharing + +import android.content.Context +import android.net.Uri +import android.util.Log +import com.google.android.gms.nearby.sharing.ConnectionResponseFrame as SharingConnectionResponseFrame +import com.google.android.gms.nearby.sharing.FileMetadata +import com.google.android.gms.nearby.sharing.Frame +import com.google.android.gms.nearby.sharing.IntroductionFrame +import com.google.android.gms.nearby.sharing.PairedKeyEncryptionFrame as SharingPairedKeyEncryptionFrame +import com.google.android.gms.nearby.sharing.PairedKeyResultFrame as SharingPairedKeyResultFrame +import com.google.android.gms.nearby.sharing.TextMetadata +import com.google.android.gms.nearby.sharing.V1Frame as SharingV1Frame +import com.google.location.nearby.connections.proto.ConnectionRequestFrame +import com.google.location.nearby.connections.proto.DisconnectionFrame +import com.google.location.nearby.connections.proto.OfflineFrame +import com.google.location.nearby.connections.proto.PayloadTransferFrame.PayloadChunk +import com.google.location.nearby.connections.proto.PayloadTransferFrame.PayloadHeader +import com.google.location.nearby.connections.proto.PayloadTransferFrame +import com.google.location.nearby.connections.proto.V1Frame +import com.google.security.cryptauth.lib.securegcm.Ukey2ClientFinished +import com.google.security.cryptauth.lib.securegcm.Ukey2ClientInit +import com.google.security.cryptauth.lib.securegcm.Ukey2HandshakeCipher +import com.google.security.cryptauth.lib.securegcm.Ukey2Message +import com.google.security.cryptauth.lib.securegcm.Ukey2ServerInit +import com.google.security.cryptauth.lib.securemessage.GenericPublicKey +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okio.ByteString +import okio.ByteString.Companion.toByteString +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.net.InetSocketAddress +import java.net.Socket +import java.security.MessageDigest +import java.security.SecureRandom + +class NearbySharingClient( + private val context: Context, + private val myDeviceName: String, + private val listener: Listener +) { + interface Listener { + fun onConnectionInitiated(targetDevice: NearbyDevice, pinCode: String) + fun onTransferAccepted() + fun onTransferProgress(bytesTransferred: Long, totalBytes: Long) + fun onTransferCompleted() + fun onError(error: Throwable) + } + + suspend fun sendFiles( + targetDevice: NearbyDevice, + files: List, + textToSend: String? = null + ) = withContext(Dispatchers.IO) { + val host = targetDevice.host ?: throw IllegalArgumentException("Target device has no host address") + val port = targetDevice.port + if (port <= 0) throw IllegalArgumentException("Target device has invalid port: $port") + + Socket().use { socket -> + try { + socket.connect(InetSocketAddress(host, port), 10000) + val input = socket.getInputStream() + val output = socket.getOutputStream() + val crypto = NearbyCrypto(isServer = false) + + // 1. Send Connection Request OfflineFrame + val myEndpointInfo = NearbyEndpointInfo(myDeviceName, NearbyDeviceType.PHONE).serialize() + val connReq = OfflineFrame( + version = OfflineFrame.Version.V1, + v1 = V1Frame( + type = V1Frame.FrameType.CONNECTION_REQUEST, + connection_request = ConnectionRequestFrame( + endpoint_id = "0000", + endpoint_name = myDeviceName, + endpoint_info = myEndpointInfo.toByteString() + ) + ) + ) + NearbySharingServer.writeLengthPrefixed(output, connReq.encode()) + + // 2. Prepare UKEY2 ClientFinish + val myGenericKey = crypto.getGenericPublicKey() + val clientFinish = Ukey2ClientFinished(public_key = myGenericKey.encode().toByteString()) + val clientFinishMsg = Ukey2Message( + message_type = Ukey2Message.Type.CLIENT_FINISH, + message_data = clientFinish.encode().toByteString() + ) + val clientFinishRaw = clientFinishMsg.encode() + + val sha512 = MessageDigest.getInstance("SHA-512") + val commitment = sha512.digest(clientFinishRaw) + + // 3. Send UKEY2 ClientInit + val random32 = ByteArray(32).apply { crypto.random.nextBytes(this) } + val clientInit = Ukey2ClientInit( + version = 1, + random = random32.toByteString(), + next_protocol = "AES_256_CBC-HMAC_SHA256", + cipher_commitments = listOf( + Ukey2ClientInit.CipherCommitment( + handshake_cipher = Ukey2HandshakeCipher.P256_SHA512, + commitment = commitment.toByteString() + ) + ) + ) + val clientInitMsg = Ukey2Message( + message_type = Ukey2Message.Type.CLIENT_INIT, + message_data = clientInit.encode().toByteString() + ) + val clientInitRaw = clientInitMsg.encode() + NearbySharingServer.writeLengthPrefixed(output, clientInitRaw) + + // 4. Receive UKEY2 ServerInit + val serverInitRaw = NearbySharingServer.readLengthPrefixed(input) + val serverInitMsg = Ukey2Message.ADAPTER.decode(serverInitRaw) + if (serverInitMsg.message_type != Ukey2Message.Type.SERVER_INIT) { + throw IOException("Expected UKEY2 ServerInit") + } + val serverInit = Ukey2ServerInit.ADAPTER.decode(serverInitMsg.message_data!!) + val serverKey = GenericPublicKey.ADAPTER.decode(serverInit.public_key!!) + + // Complete key exchange + crypto.completeKeyExchange(serverKey, clientInitRaw, serverInitRaw) + + // 5. Send UKEY2 ClientFinish + NearbySharingServer.writeLengthPrefixed(output, clientFinishRaw) + + listener.onConnectionInitiated(targetDevice, crypto.pinCode) + + // 6. Receive Connection Response + val connRespRaw = NearbySharingServer.readLengthPrefixed(input) + val connResp = OfflineFrame.ADAPTER.decode(connRespRaw) + if (connResp.v1?.connection_response?.response != com.google.location.nearby.connections.proto.ConnectionResponseFrame.ResponseStatus.ACCEPT) { + throw IOException("Connection rejected by remote party") + } + + // 7. Send Paired Key Encryption + val pairedKeyEnc = Frame( + version = Frame.Version.V1, + v1 = SharingV1Frame( + type = SharingV1Frame.FrameType.PAIRED_KEY_ENCRYPTION, + paired_key_encryption = SharingPairedKeyEncryptionFrame( + secret_id_hash = ByteArray(6).apply { crypto.random.nextBytes(this) }.toByteString(), + signed_data = ByteArray(72).apply { crypto.random.nextBytes(this) }.toByteString() + ) + ) + ) + sendEncryptedSharingFrame(output, crypto, pairedKeyEnc) + + // 8. Send Paired Key Result + val pairedKeyRes = Frame( + version = Frame.Version.V1, + v1 = SharingV1Frame( + type = SharingV1Frame.FrameType.PAIRED_KEY_RESULT, + paired_key_result = SharingPairedKeyResultFrame( + status = SharingPairedKeyResultFrame.Status.UNABLE + ) + ) + ) + sendEncryptedSharingFrame(output, crypto, pairedKeyRes) + + // 9. Send Introduction Frame + val fileMetas = mutableListOf() + val filePayloadMap = mutableMapOf() + val rng = SecureRandom() + + for (f in files) { + val pId = rng.nextLong() + filePayloadMap[f] = pId + fileMetas.add( + FileMetadata( + name = f.name, + size = f.length(), + type = FileMetadata.Type.DOCUMENT, + payload_id = pId + ) + ) + } + + val textMetas = mutableListOf() + if (textToSend != null) { + textMetas.add( + TextMetadata( + text_title = textToSend.take(20), + type = TextMetadata.Type.TEXT, + size = textToSend.toByteArray(Charsets.UTF_8).size.toLong(), + payload_id = rng.nextLong() + ) + ) + } + + val intro = Frame( + version = Frame.Version.V1, + v1 = SharingV1Frame( + type = SharingV1Frame.FrameType.INTRODUCTION, + introduction = IntroductionFrame( + file_metadata = fileMetas, + text_metadata = textMetas + ) + ) + ) + sendEncryptedSharingFrame(output, crypto, intro) + + // 10. Wait for Accept Response Frame + var accepted = false + while (!accepted) { + val encBytes = NearbySharingServer.readLengthPrefixed(input) + val offline = OfflineFrame.ADAPTER.decode(crypto.decryptSecureMessage(encBytes)) + val pt = offline.v1?.payload_transfer + if (pt != null && pt.payload_chunk?.body != null && pt.payload_chunk.body.size > 0) { + try { + val respFrame = Frame.ADAPTER.decode(pt.payload_chunk.body.toByteArray()) + if (respFrame.v1?.type == SharingV1Frame.FrameType.RESPONSE) { + val status = respFrame.v1.connection_response?.status + if (status == SharingConnectionResponseFrame.Status.ACCEPT) { + accepted = true + listener.onTransferAccepted() + } else { + throw IOException("Transfer rejected by receiver: $status") + } + } + } catch (e: Exception) { + // Non-frame payload, continue + } + } + } + + // 11. Send file payloads in chunks + val totalExpectedBytes = files.fold(0L) { acc, f -> acc + f.length() } + var totalSentBytes = 0L + + val chunkBuf = ByteArray(NearbySharingConstants.CHUNK_SIZE) + for ((f, pId) in filePayloadMap) { + FileInputStream(f).use { fis -> + var offset = 0L + var bytesRead: Int + while (fis.read(chunkBuf).also { bytesRead = it } != -1) { + val chunkData = if (bytesRead == chunkBuf.size) chunkBuf else chunkBuf.copyOf(bytesRead) + val ptFrame = PayloadTransferFrame( + packet_type = PayloadTransferFrame.PacketType.DATA, + payload_header = PayloadHeader( + id = pId, + type = PayloadHeader.PayloadType.FILE, + total_size = f.length() + ), + payload_chunk = PayloadChunk( + offset = offset, + flags = 0, + body = chunkData.toByteString() + ) + ) + val offFrame = OfflineFrame( + version = OfflineFrame.Version.V1, + v1 = V1Frame( + type = V1Frame.FrameType.PAYLOAD_TRANSFER, + payload_transfer = ptFrame + ) + ) + NearbySharingServer.writeLengthPrefixed(output, crypto.encryptOfflineFrame(offFrame.encode())) + + offset += bytesRead + totalSentBytes += bytesRead + listener.onTransferProgress(totalSentBytes, totalExpectedBytes) + } + + // Send EOF (flags = 1) + val lastPt = PayloadTransferFrame( + packet_type = PayloadTransferFrame.PacketType.DATA, + payload_header = PayloadHeader( + id = pId, + type = PayloadHeader.PayloadType.FILE, + total_size = f.length() + ), + payload_chunk = PayloadChunk( + offset = offset, + flags = 1, + body = ByteString.EMPTY + ) + ) + val lastOff = OfflineFrame( + version = OfflineFrame.Version.V1, + v1 = V1Frame( + type = V1Frame.FrameType.PAYLOAD_TRANSFER, + payload_transfer = lastPt + ) + ) + NearbySharingServer.writeLengthPrefixed(output, crypto.encryptOfflineFrame(lastOff.encode())) + } + } + + // 12. Send Disconnection + val disc = OfflineFrame( + version = OfflineFrame.Version.V1, + v1 = V1Frame( + type = V1Frame.FrameType.DISCONNECTION, + disconnection = DisconnectionFrame() + ) + ) + NearbySharingServer.writeLengthPrefixed(output, crypto.encryptOfflineFrame(disc.encode())) + listener.onTransferCompleted() + + } catch (e: Exception) { + Log.e(NearbySharingConstants.TAG, "Error during sendFiles", e) + listener.onError(e) + } + } + } + + private fun sendEncryptedSharingFrame(output: OutputStream, crypto: NearbyCrypto, frame: Frame) { + val frameBytes = frame.encode() + val payloadId = SecureRandom().nextLong() + + val transfer = PayloadTransferFrame( + packet_type = PayloadTransferFrame.PacketType.DATA, + payload_header = PayloadHeader( + id = payloadId, + type = PayloadHeader.PayloadType.BYTES, + total_size = frameBytes.size.toLong() + ), + payload_chunk = PayloadChunk( + offset = 0, + flags = 0, + body = frameBytes.toByteString() + ) + ) + val offlineFrame = OfflineFrame( + version = OfflineFrame.Version.V1, + v1 = V1Frame( + type = V1Frame.FrameType.PAYLOAD_TRANSFER, + payload_transfer = transfer + ) + ) + NearbySharingServer.writeLengthPrefixed(output, crypto.encryptOfflineFrame(offlineFrame.encode())) + + val lastChunk = transfer.copy( + payload_chunk = PayloadChunk( + offset = frameBytes.size.toLong(), + flags = 1, + body = ByteString.EMPTY + ) + ) + val offlineLast = offlineFrame.copy(v1 = offlineFrame.v1?.copy(payload_transfer = lastChunk)) + NearbySharingServer.writeLengthPrefixed(output, crypto.encryptOfflineFrame(offlineLast.encode())) + } +} diff --git a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbySharingConstants.kt b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbySharingConstants.kt new file mode 100644 index 0000000000..076d3ee0e9 --- /dev/null +++ b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbySharingConstants.kt @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.nearby.sharing + +object NearbySharingConstants { + const val TAG = "NearbySharing" + + // mDNS service configuration + const val SERVICE_TYPE = "_FC9F5ED42C8A._tcp." + val SERVICE_ID_HASH = byteArrayOf(0xFC.toByte(), 0x9F.toByte(), 0x5E.toByte()) + const val PCP_BYTE: Byte = 0x23 + + // Intents & Actions + const val ACTION_SHARE_NEARBY = "com.google.android.gms.SHARE_NEARBY" + const val ACTION_RECEIVE_NEARBY = "com.google.android.gms.RECEIVE_NEARBY" + const val ACTION_SHARING_SETTINGS = "com.google.android.gms.settings.SHARING" + const val ACTION_SEND_FOLDER = "com.google.android.gms.nearby.SEND_FOLDER" + const val ACTION_START_SERVICE = "com.google.android.gms.nearby.sharing.START_SERVICE" + const val ACTION_SERVICE_START = "com.google.android.gms.nearby.sharing.service.NearbySharingService.START" + + // Extras + const val EXTRA_PARENT_FOLDER = "com.google.android.gms.nearby.PARENT_FOLDER" + const val EXTRA_FILE_COUNT = "com.google.android.gms.nearby.FILE_COUNT" + const val EXTRA_SEND_FOLDER_CONTENT_URI = "com.google.android.gms.nearby.SEND_FOLDER_CONTENT_URI" + const val EXTRA_REQUIRED_PACKAGE = "com.google.android.gms.nearby.sharing.EXTRA_REQUIRED_PACKAGE" + + // Slice + const val SLICE_URI_STRING = "content://com.google.android.gms.nearby.sharing/scan" + + // Protocol Limits + const val MAX_FRAME_LENGTH = 5 * 1024 * 1024 + const val CHUNK_SIZE = 64 * 1024 + + // Salt and crypto constants + val D2D_SALT = byteArrayOf( + 0x82.toByte(), 0xAA.toByte(), 0x55.toByte(), 0xA0.toByte(), + 0xD3.toByte(), 0x97.toByte(), 0xF8.toByte(), 0x83.toByte(), + 0x46.toByte(), 0xCA.toByte(), 0x1C.toByte(), 0xEE.toByte(), + 0x8D.toByte(), 0x39.toByte(), 0x09.toByte(), 0xB9.toByte(), + 0x5F.toByte(), 0x13.toByte(), 0xFA.toByte(), 0x7D.toByte(), + 0xEB.toByte(), 0x1D.toByte(), 0x4A.toByte(), 0xB3.toByte(), + 0x83.toByte(), 0x76.toByte(), 0xB8.toByte(), 0x25.toByte(), + 0x6D.toByte(), 0xA8.toByte(), 0x55.toByte(), 0x10.toByte() + ) +} diff --git a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbySharingPreferences.kt b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbySharingPreferences.kt new file mode 100644 index 0000000000..2f68200eab --- /dev/null +++ b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbySharingPreferences.kt @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.nearby.sharing + +import android.content.Context +import android.os.Build +import androidx.preference.PreferenceManager + +class NearbySharingPreferences(context: Context) { + private val sp = PreferenceManager.getDefaultSharedPreferences(context) + + var deviceName: String + get() = sp.getString(KEY_DEVICE_NAME, null) ?: Build.MODEL ?: "Android Device" + set(value) = sp.edit().putString(KEY_DEVICE_NAME, value).apply() + + var enabled: Boolean + get() = sp.getBoolean(KEY_ENABLED, true) + set(value) = sp.edit().putBoolean(KEY_ENABLED, value).apply() + + var visibility: Int + get() = sp.getInt(KEY_VISIBILITY, VISIBILITY_EVERYONE) + set(value) = sp.edit().putInt(KEY_VISIBILITY, value).apply() + + companion object { + const val KEY_DEVICE_NAME = "nearby_sharing_device_name" + const val KEY_ENABLED = "nearby_sharing_enabled" + const val KEY_VISIBILITY = "nearby_sharing_visibility" + + const val VISIBILITY_EVERYONE = 0 + const val VISIBILITY_CONTACTS = 1 + const val VISIBILITY_HIDDEN = 2 + } +} diff --git a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbySharingScanner.kt b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbySharingScanner.kt new file mode 100644 index 0000000000..052f75a0f5 --- /dev/null +++ b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbySharingScanner.kt @@ -0,0 +1,129 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.nearby.sharing +import android.annotation.TargetApi +import android.content.Context +import android.net.nsd.NsdManager +import android.net.nsd.NsdServiceInfo +import android.os.Build +import android.util.Log +import java.util.Collections +import java.util.concurrent.ConcurrentHashMap + +@TargetApi(21) +class NearbySharingScanner( + private val context: Context, + private val onDeviceFound: (NearbyDevice) -> Unit, + private val onDeviceLost: (NearbyDevice) -> Unit +) { + private val nsdManager = context.getSystemService(Context.NSD_SERVICE) as? NsdManager + private var discoveryListener: NsdManager.DiscoveryListener? = null + private val discoveredDevices = ConcurrentHashMap() + + fun startDiscovery() { + if (nsdManager == null) { + Log.w(NearbySharingConstants.TAG, "NsdManager not available for discovery") + return + } + + discoveryListener = object : NsdManager.DiscoveryListener { + override fun onDiscoveryStarted(regType: String) { + Log.d(NearbySharingConstants.TAG, "mDNS discovery started for $regType") + } + + override fun onServiceFound(service: NsdServiceInfo) { + Log.d(NearbySharingConstants.TAG, "mDNS service found: ${service.serviceName}") + resolveService(service) + } + + override fun onServiceLost(service: NsdServiceInfo) { + Log.d(NearbySharingConstants.TAG, "mDNS service lost: ${service.serviceName}") + val device = discoveredDevices.remove(service.serviceName) + if (device != null) { + onDeviceLost(device) + } + } + + override fun onDiscoveryStopped(serviceType: String) { + Log.d(NearbySharingConstants.TAG, "mDNS discovery stopped") + } + + override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) { + Log.e(NearbySharingConstants.TAG, "mDNS start discovery failed: $errorCode") + stopDiscovery() + } + + override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) { + Log.e(NearbySharingConstants.TAG, "mDNS stop discovery failed: $errorCode") + } + } + + try { + nsdManager.discoverServices( + NearbySharingConstants.SERVICE_TYPE, + NsdManager.PROTOCOL_DNS_SD, + discoveryListener + ) + } catch (e: Exception) { + Log.e(NearbySharingConstants.TAG, "Error starting service discovery", e) + } + } + + private fun resolveService(serviceInfo: NsdServiceInfo) { + val resolveListener = object : NsdManager.ResolveListener { + override fun onResolveFailed(service: NsdServiceInfo, errorCode: Int) { + Log.w(NearbySharingConstants.TAG, "mDNS resolve failed: $errorCode") + } + + override fun onServiceResolved(resolvedInfo: NsdServiceInfo) { + val host = resolvedInfo.host + val port = resolvedInfo.port + val attributes = if (Build.VERSION.SDK_INT >= 21) { + resolvedInfo.attributes + } else { + emptyMap() + } + val nAttr = attributes["n"] + val endpointInfo = if (nAttr != null) { + NearbyEndpointInfo.parse(nAttr) ?: NearbyEndpointInfo.parseBase64(String(nAttr, Charsets.UTF_8)) + } else null + + val name = endpointInfo?.name ?: resolvedInfo.serviceName + val type = endpointInfo?.deviceType ?: NearbyDeviceType.PHONE + + val device = NearbyDevice( + id = resolvedInfo.serviceName, + name = name, + type = type, + host = host, + port = port, + endpointInfo = nAttr + ) + + discoveredDevices[resolvedInfo.serviceName] = device + onDeviceFound(device) + } + } + + try { + nsdManager?.resolveService(serviceInfo, resolveListener) + } catch (e: Exception) { + Log.e(NearbySharingConstants.TAG, "Error resolving service", e) + } + } + + fun stopDiscovery() { + discoveryListener?.let { + try { + nsdManager?.stopServiceDiscovery(it) + } catch (e: Exception) { + Log.e(NearbySharingConstants.TAG, "Error stopping service discovery", e) + } + discoveryListener = null + } + discoveredDevices.clear() + } +} diff --git a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbySharingServer.kt b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbySharingServer.kt new file mode 100644 index 0000000000..f2a9dd06f9 --- /dev/null +++ b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbySharingServer.kt @@ -0,0 +1,420 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.nearby.sharing + +import android.content.Context +import android.os.Environment +import android.util.Log +import com.google.android.gms.nearby.sharing.ConnectionResponseFrame as SharingConnectionResponseFrame +import com.google.android.gms.nearby.sharing.FileMetadata +import com.google.android.gms.nearby.sharing.Frame +import com.google.android.gms.nearby.sharing.IntroductionFrame +import com.google.android.gms.nearby.sharing.PairedKeyEncryptionFrame as SharingPairedKeyEncryptionFrame +import com.google.android.gms.nearby.sharing.PairedKeyResultFrame as SharingPairedKeyResultFrame +import com.google.android.gms.nearby.sharing.V1Frame as SharingV1Frame +import com.google.location.nearby.connections.proto.ConnectionResponseFrame +import com.google.location.nearby.connections.proto.DisconnectionFrame +import com.google.location.nearby.connections.proto.KeepAliveFrame +import com.google.location.nearby.connections.proto.OfflineFrame +import com.google.location.nearby.connections.proto.OsInfo +import com.google.location.nearby.connections.proto.PayloadTransferFrame.PayloadChunk +import com.google.location.nearby.connections.proto.PayloadTransferFrame.PayloadHeader +import com.google.location.nearby.connections.proto.PayloadTransferFrame +import com.google.location.nearby.connections.proto.V1Frame +import com.google.security.cryptauth.lib.securegcm.Ukey2ClientFinished +import com.google.security.cryptauth.lib.securegcm.Ukey2ClientInit +import com.google.security.cryptauth.lib.securegcm.Ukey2HandshakeCipher +import com.google.security.cryptauth.lib.securegcm.Ukey2Message +import com.google.security.cryptauth.lib.securegcm.Ukey2ServerInit +import com.google.security.cryptauth.lib.securemessage.GenericPublicKey +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import okio.ByteString +import okio.ByteString.Companion.toByteString +import java.io.EOFException +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.net.ServerSocket +import java.net.Socket +import java.security.MessageDigest +import java.security.SecureRandom + +class NearbySharingServer( + private val context: Context, + private val deviceName: String, + private val deviceType: NearbyDeviceType = NearbyDeviceType.PHONE, + private val listener: Listener +) { + interface Listener { + fun onServerStarted(port: Int) + fun onConnectionInitiated(remoteDeviceName: String, pinCode: String) + fun onTransferStarted(files: List) + fun onTransferProgress(bytesTransferred: Long, totalBytes: Long) + fun onTransferCompleted(savedFiles: List) + fun onError(error: Throwable) + } + + private var serverSocket: ServerSocket? = null + private var advertiser: NearbySharingAdvertiser? = null + private val scope = CoroutineScope(Dispatchers.IO + Job()) + private var acceptJob: Job? = null + private var isRunning = false + + fun start() { + if (isRunning) return + isRunning = true + + try { + val ss = ServerSocket(0) + serverSocket = ss + val port = ss.localPort + Log.d(NearbySharingConstants.TAG, "NearbySharingServer listening on port $port") + + advertiser = NearbySharingAdvertiser(context, deviceName, deviceType).apply { + startAdvertising(port, onStarted = { + listener.onServerStarted(port) + }, onError = { err -> + listener.onError(IOException("mDNS advertising error: $err")) + }) + } + + acceptJob = scope.launch { + while (isRunning) { + try { + val clientSocket = ss.accept() + handleClient(clientSocket) + } catch (e: Exception) { + if (isRunning) { + Log.e(NearbySharingConstants.TAG, "Server accept error", e) + listener.onError(e) + } + } + } + } + } catch (e: Exception) { + Log.e(NearbySharingConstants.TAG, "Failed to start server", e) + listener.onError(e) + } + } + + private fun handleClient(socket: Socket) { + scope.launch { + socket.use { s -> + val input = s.getInputStream() + val output = s.getOutputStream() + val crypto = NearbyCrypto(isServer = true) + + try { + // 1. Receive Connection Request OfflineFrame + val connReqBytes = readLengthPrefixed(input) + val connReqFrame = OfflineFrame.ADAPTER.decode(connReqBytes) + val remoteEndpointInfo = connReqFrame.v1?.connection_request?.endpoint_info?.toByteArray() + val remoteName = if (remoteEndpointInfo != null) { + NearbyEndpointInfo.parse(remoteEndpointInfo)?.name ?: "Nearby Sender" + } else "Nearby Sender" + + // 2. Receive UKEY2 ClientInit + val clientInitRaw = readLengthPrefixed(input) + val clientInitMsg = Ukey2Message.ADAPTER.decode(clientInitRaw) + if (clientInitMsg.message_type != Ukey2Message.Type.CLIENT_INIT) { + throw IOException("Expected UKEY2 ClientInit") + } + val clientInit = Ukey2ClientInit.ADAPTER.decode(clientInitMsg.message_data!!) + val commitment = clientInit.cipher_commitments.firstOrNull { + it.handshake_cipher == Ukey2HandshakeCipher.P256_SHA512 + }?.commitment ?: throw IOException("No supported cipher commitment in ClientInit") + + // 3. Send UKEY2 ServerInit + val myGenericKey = crypto.getGenericPublicKey() + val random32 = ByteArray(32).apply { crypto.random.nextBytes(this) } + val serverInit = Ukey2ServerInit( + version = 1, + random = random32.toByteString(), + handshake_cipher = Ukey2HandshakeCipher.P256_SHA512, + public_key = myGenericKey.encode().toByteString() + ) + val serverInitMsg = Ukey2Message( + message_type = Ukey2Message.Type.SERVER_INIT, + message_data = serverInit.encode().toByteString() + ) + val serverInitRaw = serverInitMsg.encode() + writeLengthPrefixed(output, serverInitRaw) + + // 4. Receive UKEY2 ClientFinish + val clientFinishRaw = readLengthPrefixed(input) + val sha512 = MessageDigest.getInstance("SHA-512") + val finishDigest = sha512.digest(clientFinishRaw) + if (!MessageDigest.isEqual(finishDigest, commitment.toByteArray())) { + throw SecurityException("ClientFinish cipher commitment mismatch") + } + val clientFinishMsg = Ukey2Message.ADAPTER.decode(clientFinishRaw) + val clientFinish = Ukey2ClientFinished.ADAPTER.decode(clientFinishMsg.message_data!!) + val peerKey = GenericPublicKey.ADAPTER.decode(clientFinish.public_key!!) + crypto.completeKeyExchange(peerKey, clientInitRaw, serverInitRaw) + + listener.onConnectionInitiated(remoteName, crypto.pinCode) + + // 5. Send Connection Response ACCEPT + val connResp = OfflineFrame( + version = OfflineFrame.Version.V1, + v1 = V1Frame( + type = V1Frame.FrameType.CONNECTION_RESPONSE, + connection_response = ConnectionResponseFrame( + response = ConnectionResponseFrame.ResponseStatus.ACCEPT, + status = 0, + os_info = OsInfo(type = OsInfo.OsType.ANDROID) + ) + ) + ) + writeLengthPrefixed(output, connResp.encode()) + + // 6. Encrypted session: exchange Paired Key Encryption + val pairedKeyEnc = Frame( + version = Frame.Version.V1, + v1 = SharingV1Frame( + type = SharingV1Frame.FrameType.PAIRED_KEY_ENCRYPTION, + paired_key_encryption = SharingPairedKeyEncryptionFrame( + secret_id_hash = ByteArray(6).apply { crypto.random.nextBytes(this) }.toByteString(), + signed_data = ByteArray(72).apply { crypto.random.nextBytes(this) }.toByteString() + ) + ) + ) + sendEncryptedSharingFrame(output, crypto, pairedKeyEnc) + + // 7. Send Paired Key Result + val pairedKeyRes = Frame( + version = Frame.Version.V1, + v1 = SharingV1Frame( + type = SharingV1Frame.FrameType.PAIRED_KEY_RESULT, + paired_key_result = SharingPairedKeyResultFrame( + status = SharingPairedKeyResultFrame.Status.UNABLE + ) + ) + ) + sendEncryptedSharingFrame(output, crypto, pairedKeyRes) + + // 8. Receive Introduction Frame (contains file/text list) + var introFrame: IntroductionFrame? = null + val payloadBuffers = mutableMapOf>() + + while (introFrame == null) { + val encryptedBytes = readLengthPrefixed(input) + val offline = OfflineFrame.ADAPTER.decode(crypto.decryptSecureMessage(encryptedBytes)) + val pt = offline.v1?.payload_transfer + if (pt != null && pt.payload_header?.type == PayloadHeader.PayloadType.BYTES) { + val id = pt.payload_header.id ?: 0L + val chunk = pt.payload_chunk + if (chunk != null) { + val list = payloadBuffers.getOrPut(id) { mutableListOf() } + if (chunk.body != null && chunk.body.size > 0) { + list.add(chunk.body.toByteArray()) + } + if ((chunk.flags ?: 0) and 1 == 1) { // LAST_CHUNK + val totalBytes = list.fold(0) { acc, arr -> acc + arr.size } + val fullData = ByteArray(totalBytes) + var offset = 0 + for (arr in list) { + System.arraycopy(arr, 0, fullData, offset, arr.size) + offset += arr.size + } + payloadBuffers.remove(id) + val sharingFrame = Frame.ADAPTER.decode(fullData) + if (sharingFrame.v1?.type == SharingV1Frame.FrameType.INTRODUCTION) { + introFrame = sharingFrame.v1.introduction + } + } + } + } + } + + val filesToReceive = introFrame.file_metadata + listener.onTransferStarted(filesToReceive) + + // 9. Send Accept Response Frame + val acceptResponse = Frame( + version = Frame.Version.V1, + v1 = SharingV1Frame( + type = SharingV1Frame.FrameType.RESPONSE, + connection_response = SharingConnectionResponseFrame( + status = SharingConnectionResponseFrame.Status.ACCEPT + ) + ) + ) + sendEncryptedSharingFrame(output, crypto, acceptResponse) + + // 10. Receive Payload Chunks for Files + val downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + ?: context.cacheDir + if (!downloadDir.exists()) downloadDir.mkdirs() + + val targetFiles = mutableListOf() + val fileStreams = mutableMapOf() + val fileRemaining = mutableMapOf() + var totalExpectedBytes = filesToReceive.fold(0L) { acc, fm -> acc + (fm.size ?: 0L) } + var totalReceivedBytes = 0L + + for (fm in filesToReceive) { + val name = fm.name ?: "received_file_${System.currentTimeMillis()}" + val targetFile = File(downloadDir, name) + targetFiles.add(targetFile) + val id = fm.payload_id ?: 0L + fileStreams[id] = FileOutputStream(targetFile) + fileRemaining[id] = fm.size ?: 0L + } + + var allComplete = targetFiles.isEmpty() + while (!allComplete) { + val encryptedBytes = readLengthPrefixed(input) + val offline = OfflineFrame.ADAPTER.decode(crypto.decryptSecureMessage(encryptedBytes)) + val pt = offline.v1?.payload_transfer + if (pt != null) { + val id = pt.payload_header?.id ?: 0L + val chunk = pt.payload_chunk + val fos = fileStreams[id] + if (fos != null && chunk != null) { + if (chunk.body != null && chunk.body.size > 0) { + val b = chunk.body.toByteArray() + fos.write(b) + totalReceivedBytes += b.size + listener.onTransferProgress(totalReceivedBytes, totalExpectedBytes) + } + if ((chunk.flags ?: 0) and 1 == 1) { // LAST_CHUNK + fos.flush() + fos.close() + fileStreams.remove(id) + if (fileStreams.isEmpty()) { + allComplete = true + } + } + } + } else if (offline.v1?.type == V1Frame.FrameType.KEEP_ALIVE) { + val ack = OfflineFrame( + version = OfflineFrame.Version.V1, + v1 = V1Frame( + type = V1Frame.FrameType.KEEP_ALIVE, + keep_alive = KeepAliveFrame(ack = true) + ) + ) + val ackEnc = crypto.encryptOfflineFrame(ack.encode()) + writeLengthPrefixed(output, ackEnc) + } else if (offline.v1?.type == V1Frame.FrameType.DISCONNECTION) { + break + } + } + + // Send clean disconnection + val disc = OfflineFrame( + version = OfflineFrame.Version.V1, + v1 = V1Frame( + type = V1Frame.FrameType.DISCONNECTION, + disconnection = DisconnectionFrame() + ) + ) + writeLengthPrefixed(output, crypto.encryptOfflineFrame(disc.encode())) + listener.onTransferCompleted(targetFiles) + + } catch (e: Exception) { + Log.e(NearbySharingConstants.TAG, "Error handling nearby client", e) + listener.onError(e) + } + } + } + } + + private fun sendEncryptedSharingFrame(output: OutputStream, crypto: NearbyCrypto, frame: Frame) { + val frameBytes = frame.encode() + val payloadId = SecureRandom().nextLong() + + val transfer = PayloadTransferFrame( + packet_type = PayloadTransferFrame.PacketType.DATA, + payload_header = PayloadHeader( + id = payloadId, + type = PayloadHeader.PayloadType.BYTES, + total_size = frameBytes.size.toLong() + ), + payload_chunk = PayloadChunk( + offset = 0, + flags = 0, + body = frameBytes.toByteString() + ) + ) + val offlineFrame = OfflineFrame( + version = OfflineFrame.Version.V1, + v1 = V1Frame( + type = V1Frame.FrameType.PAYLOAD_TRANSFER, + payload_transfer = transfer + ) + ) + val enc1 = crypto.encryptOfflineFrame(offlineFrame.encode()) + writeLengthPrefixed(output, enc1) + + val lastChunk = transfer.copy( + payload_chunk = PayloadChunk( + offset = frameBytes.size.toLong(), + flags = 1, + body = ByteString.EMPTY + ) + ) + val offlineLast = offlineFrame.copy(v1 = offlineFrame.v1?.copy(payload_transfer = lastChunk)) + val enc2 = crypto.encryptOfflineFrame(offlineLast.encode()) + writeLengthPrefixed(output, enc2) + } + + fun stop() { + isRunning = false + acceptJob?.cancel() + advertiser?.stopAdvertising() + try { + serverSocket?.close() + } catch (e: Exception) { + Log.e(NearbySharingConstants.TAG, "Error closing server socket", e) + } + } + + companion object { + fun readFully(input: InputStream, buffer: ByteArray) { + var read = 0 + while (read < buffer.size) { + val count = input.read(buffer, read, buffer.size - read) + if (count == -1) throw EOFException("Unexpected EOF while reading frame") + read += count + } + } + + fun readLengthPrefixed(input: InputStream): ByteArray { + val lenBuf = ByteArray(4) + readFully(input, lenBuf) + val len = ((lenBuf[0].toInt() and 0xFF) shl 24) or + ((lenBuf[1].toInt() and 0xFF) shl 16) or + ((lenBuf[2].toInt() and 0xFF) shl 8) or + (lenBuf[3].toInt() and 0xFF) + if (len <= 0 || len > NearbySharingConstants.MAX_FRAME_LENGTH) { + throw IOException("Invalid frame length: $len") + } + val buf = ByteArray(len) + readFully(input, buf) + return buf + } + + fun writeLengthPrefixed(output: OutputStream, bytes: ByteArray) { + val len = bytes.size + val header = byteArrayOf( + (len ushr 24).toByte(), + (len ushr 16).toByte(), + (len ushr 8).toByte(), + len.toByte() + ) + output.write(header) + output.write(bytes) + output.flush() + } + } +} diff --git a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbySharingService.kt b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbySharingService.kt new file mode 100644 index 0000000000..3d905b260a --- /dev/null +++ b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/NearbySharingService.kt @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.nearby.sharing + +import android.os.Binder +import android.os.IBinder +import android.util.Log +import com.google.android.gms.common.Feature +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.internal.ConnectionInfo +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import org.microg.gms.BaseService +import org.microg.gms.common.GmsService +import org.microg.gms.common.PackageUtils + +class NearbySharingService : BaseService( + NearbySharingConstants.TAG, + GmsService.NEARBY, + GmsService.NEARBY_SHARING +) { + override fun handleServiceRequest( + callback: IGmsCallbacks, + request: GetServiceRequest, + service: GmsService + ) { + PackageUtils.getAndCheckCallingPackage(this, request.packageName) + Log.d(NearbySharingConstants.TAG, "handleServiceRequest: ${request.packageName} for $service") + + val binder = object : Binder() {} + val info = ConnectionInfo().apply { + features = arrayOf( + Feature("nearby_sharing", 1), + Feature("nearby_sharing_get_phone_numbers", 1) + ) + } + callback.onPostInitCompleteWithConnectionInfo(CommonStatusCodes.SUCCESS, binder, info) + } +} diff --git a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/ui/NearbySharingActivity.kt b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/ui/NearbySharingActivity.kt new file mode 100644 index 0000000000..4340531ab9 --- /dev/null +++ b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/ui/NearbySharingActivity.kt @@ -0,0 +1,187 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.nearby.sharing.ui + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.View +import android.widget.Button +import android.widget.LinearLayout +import android.widget.ProgressBar +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch +import org.microg.gms.nearby.core.R +import org.microg.gms.nearby.sharing.NearbyDevice +import org.microg.gms.nearby.sharing.NearbySharingClient +import org.microg.gms.nearby.sharing.NearbySharingPreferences +import org.microg.gms.nearby.sharing.NearbySharingScanner +import java.io.File +import java.io.FileOutputStream + +class NearbySharingActivity : AppCompatActivity() { + private var scanner: NearbySharingScanner? = null + private var client: NearbySharingClient? = null + private lateinit var prefs: NearbySharingPreferences + + private lateinit var statusText: TextView + private lateinit var searchProgress: ProgressBar + private lateinit var pinText: TextView + private lateinit var transferProgress: ProgressBar + private lateinit var devicesContainer: LinearLayout + private lateinit var btnCancel: Button + + private val filesToSend = mutableListOf() + private var textToSend: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.nearby_sharing_activity) + + prefs = NearbySharingPreferences(this) + statusText = findViewById(R.id.status_text) + searchProgress = findViewById(R.id.search_progress) + pinText = findViewById(R.id.pin_text) + transferProgress = findViewById(R.id.transfer_progress) + devicesContainer = findViewById(R.id.devices_container) + btnCancel = findViewById(R.id.btn_cancel) + + btnCancel.setOnClickListener { finish() } + + parseIntent(intent) + + startDiscovery() + } + + private fun parseIntent(intent: Intent?) { + if (intent == null) return + + if (intent.hasExtra(Intent.EXTRA_TEXT)) { + textToSend = intent.getStringExtra(Intent.EXTRA_TEXT) + } + + val streamUri = intent.getParcelableExtra(Intent.EXTRA_STREAM) + if (streamUri != null) { + resolveUriToFile(streamUri)?.let { filesToSend.add(it) } + } + + val streamUris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM) + if (streamUris != null) { + for (uri in streamUris) { + resolveUriToFile(uri)?.let { filesToSend.add(it) } + } + } + } + + private fun resolveUriToFile(uri: Uri): File? { + if (uri.scheme == "file") { + return uri.path?.let { File(it) } + } + return try { + val contentResolver = contentResolver + val inputStream = contentResolver.openInputStream(uri) ?: return null + val fileName = uri.lastPathSegment ?: "shared_file_${System.currentTimeMillis()}" + val tempFile = File(cacheDir, fileName) + tempFile.outputStream().use { fos -> + inputStream.copyTo(fos) + } + tempFile + } catch (e: Exception) { + null + } + } + + private fun startDiscovery() { + scanner = NearbySharingScanner( + this, + onDeviceFound = { device -> + runOnUiThread { addDeviceView(device) } + }, + onDeviceLost = { device -> + runOnUiThread { removeDeviceView(device) } + } + ) + scanner?.startDiscovery() + } + + private fun addDeviceView(device: NearbyDevice) { + val view = TextView(this).apply { + text = "📱 ${device.name}" + textSize = 18f + setPadding(16, 24, 16, 24) + setBackgroundResource(android.R.drawable.list_selector_background) + setOnClickListener { + startTransfer(device) + } + } + view.tag = device.id + devicesContainer.addView(view) + } + + private fun removeDeviceView(device: NearbyDevice) { + val child = devicesContainer.findViewWithTag(device.id) + if (child != null) { + devicesContainer.removeView(child) + } + } + + private fun startTransfer(device: NearbyDevice) { + scanner?.stopDiscovery() + devicesContainer.visibility = View.GONE + searchProgress.visibility = View.GONE + statusText.text = getString(R.string.nearby_share_connecting, device.name) + + client = NearbySharingClient(this, prefs.deviceName, object : NearbySharingClient.Listener { + override fun onConnectionInitiated(targetDevice: NearbyDevice, pinCode: String) { + runOnUiThread { + pinText.visibility = View.VISIBLE + pinText.text = getString(R.string.nearby_share_verify_pin, pinCode) + } + } + + override fun onTransferAccepted() { + runOnUiThread { + statusText.text = getString(R.string.nearby_share_sending) + transferProgress.visibility = View.VISIBLE + } + } + + override fun onTransferProgress(bytesTransferred: Long, totalBytes: Long) { + runOnUiThread { + if (totalBytes > 0) { + transferProgress.progress = ((bytesTransferred * 100) / totalBytes).toInt() + } + } + } + + override fun onTransferCompleted() { + runOnUiThread { + statusText.text = getString(R.string.nearby_share_sent) + transferProgress.progress = 100 + btnCancel.text = getString(android.R.string.ok) + } + } + + override fun onError(error: Throwable) { + runOnUiThread { + statusText.text = "Error: ${error.localizedMessage}" + pinText.visibility = View.GONE + } + } + }) + + lifecycleScope.launch { + client?.sendFiles(device, filesToSend, textToSend) + } + } + + override fun onDestroy() { + super.onDestroy() + scanner?.stopDiscovery() + } +} diff --git a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/ui/NearbySharingSettingsActivity.kt b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/ui/NearbySharingSettingsActivity.kt new file mode 100644 index 0000000000..c259313403 --- /dev/null +++ b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/ui/NearbySharingSettingsActivity.kt @@ -0,0 +1,85 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.nearby.sharing.ui + +import android.os.Bundle +import android.widget.EditText +import android.widget.LinearLayout +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import org.microg.gms.nearby.core.R +import org.microg.gms.nearby.sharing.NearbySharingPreferences + +class NearbySharingSettingsActivity : AppCompatActivity() { + private lateinit var prefs: NearbySharingPreferences + private lateinit var textDeviceName: TextView + private lateinit var textVisibility: TextView + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.nearby_settings_activity) + + prefs = NearbySharingPreferences(this) + textDeviceName = findViewById(R.id.text_device_name) + textVisibility = findViewById(R.id.text_visibility) + + findViewById(R.id.row_device_name).setOnClickListener { + showDeviceNameDialog() + } + + findViewById(R.id.row_visibility).setOnClickListener { + showVisibilityDialog() + } + + updateUi() + } + + private fun updateUi() { + textDeviceName.text = prefs.deviceName + textVisibility.text = when (prefs.visibility) { + NearbySharingPreferences.VISIBILITY_EVERYONE -> getString(R.string.nearby_share_visibility_everyone) + NearbySharingPreferences.VISIBILITY_CONTACTS -> getString(R.string.nearby_share_visibility_contacts) + else -> getString(R.string.nearby_share_visibility_hidden) + } + } + + private fun showDeviceNameDialog() { + val input = EditText(this).apply { + setText(prefs.deviceName) + setSelection(text.length) + } + AlertDialog.Builder(this) + .setTitle(R.string.nearby_share_device_name) + .setView(input) + .setPositiveButton(android.R.string.ok) { _, _ -> + val newName = input.text.toString().trim() + if (newName.isNotEmpty()) { + prefs.deviceName = newName + updateUi() + } + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun showVisibilityDialog() { + val options = arrayOf( + getString(R.string.nearby_share_visibility_everyone), + getString(R.string.nearby_share_visibility_contacts), + getString(R.string.nearby_share_visibility_hidden) + ) + AlertDialog.Builder(this) + .setTitle(R.string.nearby_share_visibility) + .setSingleChoiceItems(options, prefs.visibility) { dialog, which -> + prefs.visibility = which + updateUi() + dialog.dismiss() + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } +} diff --git a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/ui/NearbySharingSliceProvider.kt b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/ui/NearbySharingSliceProvider.kt new file mode 100644 index 0000000000..4421ef5b2b --- /dev/null +++ b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/ui/NearbySharingSliceProvider.kt @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.nearby.sharing.ui + +import android.content.ContentProvider +import android.content.ContentValues +import android.database.Cursor +import android.net.Uri +import android.os.Bundle + +class NearbySharingSliceProvider : ContentProvider() { + override fun onCreate(): Boolean = true + + override fun query( + uri: Uri, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String? + ): Cursor? = null + + override fun getType(uri: Uri): String = "vnd.android.slice" + + override fun insert(uri: Uri, values: ContentValues?): Uri? = null + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = 0 + + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array? + ): Int = 0 + + override fun call(method: String, arg: String?, extras: Bundle?): Bundle? { + return Bundle() + } +} diff --git a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/ui/ReceiveActivity.kt b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/ui/ReceiveActivity.kt new file mode 100644 index 0000000000..306df7c1c0 --- /dev/null +++ b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/sharing/ui/ReceiveActivity.kt @@ -0,0 +1,102 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.nearby.sharing.ui + +import android.os.Bundle +import android.view.View +import android.widget.Button +import android.widget.ProgressBar +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import com.google.android.gms.nearby.sharing.FileMetadata +import org.microg.gms.nearby.core.R +import org.microg.gms.nearby.sharing.NearbySharingPreferences +import org.microg.gms.nearby.sharing.NearbySharingServer +import java.io.File + +class ReceiveActivity : AppCompatActivity() { + private var server: NearbySharingServer? = null + private lateinit var prefs: NearbySharingPreferences + + private lateinit var visibleAsText: TextView + private lateinit var waitingProgress: ProgressBar + private lateinit var pinText: TextView + private lateinit var transferProgress: ProgressBar + private lateinit var statusText: TextView + private lateinit var btnDone: Button + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.nearby_receive_activity) + + prefs = NearbySharingPreferences(this) + visibleAsText = findViewById(R.id.visible_as_text) + waitingProgress = findViewById(R.id.waiting_progress) + pinText = findViewById(R.id.pin_text) + transferProgress = findViewById(R.id.transfer_progress) + statusText = findViewById(R.id.status_text) + btnDone = findViewById(R.id.btn_done) + + visibleAsText.text = getString(R.string.nearby_share_waiting_receive, prefs.deviceName) + btnDone.setOnClickListener { finish() } + + startServer() + } + + private fun startServer() { + server = NearbySharingServer(this, prefs.deviceName, listener = object : NearbySharingServer.Listener { + override fun onServerStarted(port: Int) { + runOnUiThread { + visibleAsText.text = getString(R.string.nearby_share_waiting_receive, prefs.deviceName) + } + } + + override fun onConnectionInitiated(remoteDeviceName: String, pinCode: String) { + runOnUiThread { + pinText.visibility = View.VISIBLE + pinText.text = getString(R.string.nearby_share_verify_pin, pinCode) + statusText.visibility = View.VISIBLE + statusText.text = getString(R.string.nearby_share_receiving, remoteDeviceName) + } + } + + override fun onTransferStarted(files: List) { + runOnUiThread { + waitingProgress.visibility = View.GONE + transferProgress.visibility = View.VISIBLE + } + } + + override fun onTransferProgress(bytesTransferred: Long, totalBytes: Long) { + runOnUiThread { + if (totalBytes > 0) { + transferProgress.progress = ((bytesTransferred * 100) / totalBytes).toInt() + } + } + } + + override fun onTransferCompleted(savedFiles: List) { + runOnUiThread { + transferProgress.progress = 100 + statusText.text = getString(R.string.nearby_share_sent) + ": ${savedFiles.size} file(s) saved" + } + } + + override fun onError(error: Throwable) { + runOnUiThread { + statusText.visibility = View.VISIBLE + statusText.text = "Error: ${error.localizedMessage}" + } + } + }) + server?.start() + } + + override fun onDestroy() { + super.onDestroy() + server?.stop() + } +} diff --git a/play-services-nearby/core/src/main/proto/device_to_device_messages.proto b/play-services-nearby/core/src/main/proto/device_to_device_messages.proto new file mode 100644 index 0000000000..5600373e9e --- /dev/null +++ b/play-services-nearby/core/src/main/proto/device_to_device_messages.proto @@ -0,0 +1,81 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package securegcm; + +import "securemessage.proto"; + +option optimize_for = LITE_RUNTIME; +option java_package = "com.google.security.cryptauth.lib.securegcm"; +option java_outer_classname = "DeviceToDeviceMessagesProto"; +option objc_class_prefix = "SGCM"; + +// Used by protocols between devices +message DeviceToDeviceMessage { + // the payload of the message + optional bytes message = 1; + + // the sequence number of the message - must be increasing. + optional int32 sequence_number = 2; +} + +// sent as the first message from initiator to responder +// in an unauthenticated Diffie-Hellman Key Exchange +message InitiatorHello { + // The session public key to send to the responder + optional securemessage.GenericPublicKey public_dh_key = 1; + + // The protocol version + optional int32 protocol_version = 2 [default = 0]; +} + +// sent inside the header of the first message from the responder to the +// initiator in an unauthenticated Diffie-Hellman Key Exchange +message ResponderHello { + // The session public key to send to the initiator + optional securemessage.GenericPublicKey public_dh_key = 1; + + // The protocol version + optional int32 protocol_version = 2 [default = 0]; +} + +// Type of curve +enum Curve { ED_25519 = 1; } + +// A convenience proto for encoding curve points in affine representation +message EcPoint { + required Curve curve = 1; + + // x and y are encoded in big-endian two's complement + // client MUST verify (x,y) is a valid point on the specified curve + required bytes x = 2; + required bytes y = 3; +} + +message SpakeHandshakeMessage { + // Each flow in the protocol bumps this counter + optional int32 flow_number = 1; + + // Some (but not all) SPAKE flows send a point on an elliptic curve + optional EcPoint ec_point = 2; + + // Some (but not all) SPAKE flows send a hash value + optional bytes hash_value = 3; + + // The last flow of a SPAKE protocol can send an optional payload, + // since the key exchange is already complete on the sender's side. + optional bytes payload = 4; +} diff --git a/play-services-nearby/core/src/main/proto/offline_wire_formats.proto b/play-services-nearby/core/src/main/proto/offline_wire_formats.proto new file mode 100644 index 0000000000..afd0ed70b6 --- /dev/null +++ b/play-services-nearby/core/src/main/proto/offline_wire_formats.proto @@ -0,0 +1,596 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package location.nearby.connections; + +// import "storage/datapol/annotations/proto/semantic_annotations.proto"; + +option optimize_for = LITE_RUNTIME; +option java_outer_classname = "OfflineWireFormatsProto"; +option java_package = "com.google.location.nearby.connections.proto"; +option objc_class_prefix = "GNCP"; + +message OfflineFrame { + enum Version { + UNKNOWN_VERSION = 0; + V1 = 1; + } + optional Version version = 1; + + // Right now there's only 1 version, but if there are more, exactly one of + // the following fields will be set. + optional V1Frame v1 = 2; +} + +message V1Frame { + enum FrameType { + UNKNOWN_FRAME_TYPE = 0; + CONNECTION_REQUEST = 1; + CONNECTION_RESPONSE = 2; + PAYLOAD_TRANSFER = 3; + BANDWIDTH_UPGRADE_NEGOTIATION = 4; + KEEP_ALIVE = 5; + DISCONNECTION = 6; + PAIRED_KEY_ENCRYPTION = 7; + AUTHENTICATION_MESSAGE = 8; + AUTHENTICATION_RESULT = 9; + AUTO_RESUME = 10; + AUTO_RECONNECT = 11; + BANDWIDTH_UPGRADE_RETRY = 12; + } + optional FrameType type = 1; + + // Exactly one of the following fields will be set. + optional ConnectionRequestFrame connection_request = 2; + optional ConnectionResponseFrame connection_response = 3; + optional PayloadTransferFrame payload_transfer = 4; + optional BandwidthUpgradeNegotiationFrame bandwidth_upgrade_negotiation = 5; + optional KeepAliveFrame keep_alive = 6; + optional DisconnectionFrame disconnection = 7; + optional PairedKeyEncryptionFrame paired_key_encryption = 8; + optional AuthenticationMessageFrame authentication_message = 9; + optional AuthenticationResultFrame authentication_result = 10; + optional AutoResumeFrame auto_resume = 11; + optional AutoReconnectFrame auto_reconnect = 12; + optional BandwidthUpgradeRetryFrame bandwidth_upgrade_retry = 13; +} + +message ConnectionRequestFrame { + // Should always match cs/symbol:location.nearby.proto.connections.Medium + // LINT.IfChange + enum Medium { + UNKNOWN_MEDIUM = 0; + MDNS = 1 [deprecated = true]; + BLUETOOTH = 2; + WIFI_HOTSPOT = 3; + BLE = 4; + WIFI_LAN = 5; + WIFI_AWARE = 6; + NFC = 7; + WIFI_DIRECT = 8; + WEB_RTC = 9; + BLE_L2CAP = 10; + USB = 11; + WEB_RTC_NON_CELLULAR = 12; + AWDL = 13; + } + // LINT.ThenChange(//depot/google3/third_party/nearby/proto/connections_enums.proto) + + // LINT.IfChange + enum ConnectionMode { + LEGACY = 0; + INSTANT = 1; + } + // LINT.ThenChange(//depot/google3/third_party/nearby/proto/connections_enums.proto) + + optional string endpoint_id = 1; + optional string endpoint_name = 2; + optional bytes handshake_data = 3; + // A random number generated for each outgoing connection that is presently + // used to act as a tiebreaker when 2 devices connect to each other + // simultaneously; this can also be used for other initialization-scoped + // things in the future. + optional int32 nonce = 4; + // The mediums this device supports upgrading to. This list should be filtered + // by both the strategy and this device's individual limitations. + repeated Medium mediums = 5; + optional bytes endpoint_info = 6; + optional MediumMetadata medium_metadata = 7; + optional int32 keep_alive_interval_millis = 8; + optional int32 keep_alive_timeout_millis = 9; + // The type of {@link Device} object. + optional int32 device_type = 10 [default = 0, deprecated = true]; + // The bytes of serialized {@link Device} object. + optional bytes device_info = 11 [deprecated = true]; + // Represents the {@link Device} that invokes the request. + oneof Device { + ConnectionsDevice connections_device = 12; + PresenceDevice presence_device = 13; + } + optional ConnectionMode connection_mode = 14; + optional LocationHint location_hint = 15; +} + +message ConnectionResponseFrame { + // This doesn't need to send back endpoint_id and endpoint_name (like + // the ConnectionRequestFrame does) because those have already been + // transmitted out-of-band, at the time this endpoint was discovered. + + // One of: + // + // - ConnectionsStatusCodes.STATUS_OK + // - ConnectionsStatusCodes.STATUS_CONNECTION_REJECTED. + optional int32 status = 1 [deprecated = true]; + optional bytes handshake_data = 2; + + // Used to replace the status integer parameter with a meaningful enum item. + // Map ConnectionsStatusCodes.STATUS_OK to ACCEPT and + // ConnectionsStatusCodes.STATUS_CONNECTION_REJECTED to REJECT. + // Flag: connection_replace_status_with_response_connectionResponseFrame + enum ResponseStatus { + UNKNOWN_RESPONSE_STATUS = 0; + ACCEPT = 1; + REJECT = 2; + } + optional ResponseStatus response = 3; + optional OsInfo os_info = 4; + // A bitmask value to indicate which medium supports Multiplex transmission + // feature. Each supporting medium could utilize one bit starting from the + // least significant bit in this field. eq. BT utilizes the LSB bit which 0x01 + // means bt supports multiplex while 0x00 means not. Refer to ClientProxy.java + // for the bit usages. + optional int32 multiplex_socket_bitmask = 5; + optional int32 nearby_connections_version = 6 [deprecated = true]; + optional int32 safe_to_disconnect_version = 7; + optional LocationHint location_hint = 8; + optional int32 keep_alive_timeout_millis = 9; +} + +message PayloadTransferFrame { + enum PacketType { + UNKNOWN_PACKET_TYPE = 0; + DATA = 1; + CONTROL = 2; + PAYLOAD_ACK = 3; + } + + message PayloadHeader { + enum PayloadType { + UNKNOWN_PAYLOAD_TYPE = 0; + BYTES = 1; + FILE = 2; + STREAM = 3; + } + optional int64 id = 1; + optional PayloadType type = 2; + optional int64 total_size = 3; + optional bool is_sensitive = 4; + optional string file_name = 5; + optional string parent_folder = 6; + // Time since the epoch in milliseconds. + optional int64 last_modified_timestamp_millis = 7; + } + + // Accompanies DATA packets. + message PayloadChunk { + enum Flags { + LAST_CHUNK = 0x1; + } + optional int32 flags = 1; + optional int64 offset = 2; + optional bytes body = 3; + optional int32 index = 4; + } + + // Accompanies CONTROL packets. + message ControlMessage { + enum EventType { + UNKNOWN_EVENT_TYPE = 0; + PAYLOAD_ERROR = 1; + PAYLOAD_CANCELED = 2; + // Use PacketType.PAYLOAD_ACK instead + PAYLOAD_RECEIVED_ACK = 3 [deprecated = true]; + } + + optional EventType event = 1; + optional int64 offset = 2; + } + + optional PacketType packet_type = 1; + optional PayloadHeader payload_header = 2; + + // Exactly one of the following fields will be set, depending on the type. + optional PayloadChunk payload_chunk = 3; + optional ControlMessage control_message = 4; +} + +message BandwidthUpgradeNegotiationFrame { + enum EventType { + UNKNOWN_EVENT_TYPE = 0; + UPGRADE_PATH_AVAILABLE = 1; + LAST_WRITE_TO_PRIOR_CHANNEL = 2; + SAFE_TO_CLOSE_PRIOR_CHANNEL = 3; + CLIENT_INTRODUCTION = 4; + UPGRADE_FAILURE = 5; + CLIENT_INTRODUCTION_ACK = 6; + // The event type that requires the remote device to send the available + // upgrade path. + UPGRADE_PATH_REQUEST = 7; + } + + // Accompanies UPGRADE_PATH_AVAILABLE and UPGRADE_FAILURE events. + message UpgradePathInfo { + // Should always match cs/symbol:location.nearby.proto.connections.Medium + enum Medium { + UNKNOWN_MEDIUM = 0; + MDNS = 1 [deprecated = true]; + BLUETOOTH = 2; + WIFI_HOTSPOT = 3; + BLE = 4; + WIFI_LAN = 5; + WIFI_AWARE = 6; + NFC = 7; + WIFI_DIRECT = 8; + WEB_RTC = 9; + // 10 is reserved. + USB = 11; + WEB_RTC_NON_CELLULAR = 12; + AWDL = 13; + } + + // Accompanies Medium.WIFI_HOTSPOT. + message WifiHotspotCredentials { + optional string ssid = 1; + optional string password = 2 + /* type = ST_ACCOUNT_CREDENTIAL */; + optional int32 port = 3; + optional string gateway = 4 [default = "0.0.0.0"]; + // This field can be a band or frequency + optional int32 frequency = 5 [default = -1]; + } + + // Accompanies Medium.WIFI_LAN. + message WifiLanSocket { + optional bytes ip_address = 1; + optional int32 wifi_port = 2; + } + + // Accompanies Medium.BLUETOOTH. + message BluetoothCredentials { + optional string service_name = 1; + optional string mac_address = 2; + } + + // Accompanies Medium.WIFI_AWARE. + message WifiAwareCredentials { + optional string service_id = 1; + optional bytes service_info = 2; + optional string password = 3 + /* type = ST_ACCOUNT_CREDENTIAL */; + } + + // Accompanies Medium.WIFI_DIRECT. + message WifiDirectCredentials { + optional string ssid = 1; + optional string password = 2 + /* type = ST_ACCOUNT_CREDENTIAL */; + optional int32 port = 3; + optional int32 frequency = 4; + optional string gateway = 5 [default = "0.0.0.0"]; + // IPv6 link-local address, network order (128bits). + // The GO should listen on both IPv4 and IPv6 addresses. + // https://en.wikipedia.org/wiki/Link-local_address#IPv6 + optional bytes ip_v6_address = 6; + } + + // Accompanies Medium.WEB_RTC + message WebRtcCredentials { + optional string peer_id = 1; + optional LocationHint location_hint = 2; + } + + // Accompanies Medium.AWDL. + message AwdlCredentials { + optional string service_name = 1; + optional string service_type = 2; + optional string password = 3 + /* type = ST_ACCOUNT_CREDENTIAL */; + } + + message UpgradePathRequest { + // Supported mediums on the advertiser device. + repeated Medium mediums = 1 [packed = true]; + optional MediumMetadata medium_meta_data = 2; + } + + optional Medium medium = 1; + + // Exactly one of the following fields will be set. + optional WifiHotspotCredentials wifi_hotspot_credentials = 2; + optional WifiLanSocket wifi_lan_socket = 3; + optional BluetoothCredentials bluetooth_credentials = 4; + optional WifiAwareCredentials wifi_aware_credentials = 5; + optional WifiDirectCredentials wifi_direct_credentials = 6; + optional WebRtcCredentials web_rtc_credentials = 8; + optional AwdlCredentials awdl_credentials = 11; + + // Disable Encryption for this upgrade medium to improve throughput. + optional bool supports_disabling_encryption = 7; + + // An ack will be sent after the CLIENT_INTRODUCTION frame. + optional bool supports_client_introduction_ack = 9; + + optional UpgradePathRequest upgrade_path_request = 10; + } + + // Accompanies SAFE_TO_CLOSE_PRIOR_CHANNEL events. + message SafeToClosePriorChannel { + optional int32 sta_frequency = 1; + } + + // Accompanies CLIENT_INTRODUCTION events. + message ClientIntroduction { + optional string endpoint_id = 1; + optional bool supports_disabling_encryption = 2; + optional string last_endpoint_id = 3; + } + + // Accompanies CLIENT_INTRODUCTION_ACK events. + message ClientIntroductionAck {} + + optional EventType event_type = 1; + + // Exactly one of the following fields will be set. + optional UpgradePathInfo upgrade_path_info = 2; + optional ClientIntroduction client_introduction = 3; + optional ClientIntroductionAck client_introduction_ack = 4; + optional SafeToClosePriorChannel safe_to_close_prior_channel = 5; +} + +message BandwidthUpgradeRetryFrame { + // Should always match cs/symbol:location.nearby.proto.connections.Medium + // LINT.IfChange + enum Medium { + UNKNOWN_MEDIUM = 0; + // 1 is reserved. + BLUETOOTH = 2; + WIFI_HOTSPOT = 3; + BLE = 4; + WIFI_LAN = 5; + WIFI_AWARE = 6; + NFC = 7; + WIFI_DIRECT = 8; + WEB_RTC = 9; + BLE_L2CAP = 10; + USB = 11; + WEB_RTC_NON_CELLULAR = 12; + AWDL = 13; + } + // LINT.ThenChange(//depot/google3/third_party/nearby/proto/connections_enums.proto) + + // The mediums this device supports upgrading to. This list should be filtered + // by both the strategy and this device's individual limitations. + repeated Medium supported_medium = 1; + + // If true, expect the remote endpoint to send back the latest + // supported_medium. + optional bool is_request = 2; +} + +message KeepAliveFrame { + // And ack will be sent after receiving KEEP_ALIVE frame. + optional bool ack = 1; + // The sequence number + optional uint32 seq_num = 2; +} + +// Informs the remote side to immediately severe the socket connection. +// Used in bandwidth upgrades to get around a race condition, but may be used +// in other situations to trigger a faster disconnection event than waiting for +// socket closed on the remote side. +message DisconnectionFrame { + // Apply safe-to-disconnect protocol if true. + optional bool request_safe_to_disconnect = 1; + + // Ack of receiving Disconnection frame will be sent to the sender + // frame. + optional bool ack_safe_to_disconnect = 2; +} + +// A paired key encryption packet sent between devices, contains signed data. +message PairedKeyEncryptionFrame { + // The encrypted data (raw authentication token for the established + // connection) in byte array format. + optional bytes signed_data = 1; +} + +// Nearby Connections authentication frame, contains the bytes format of a +// DeviceProvider's authentication message. +message AuthenticationMessageFrame { + // An auth message generated by DeviceProvider. + // To be sent to the remote device for verification during connection setups. + optional bytes auth_message = 1; +} + +// Nearby Connections authentication result frame. +message AuthenticationResultFrame { + // The authentication result. Non null if this frame is used to exchange + // authentication result. + optional int32 result = 1; +} + +message AutoResumeFrame { + enum EventType { + UNKNOWN_AUTO_RESUME_EVENT_TYPE = 0; + PAYLOAD_RESUME_TRANSFER_START = 1; + PAYLOAD_RESUME_TRANSFER_ACK = 2; + } + + optional EventType event_type = 1; + optional int64 pending_payload_id = 2; + optional int32 next_payload_chunk_index = 3; + optional int32 version = 4; +} + +message AutoReconnectFrame { + enum EventType { + UNKNOWN_EVENT_TYPE = 0; + CLIENT_INTRODUCTION = 1; + CLIENT_INTRODUCTION_ACK = 2; + } + optional string endpoint_id = 1; + optional EventType event_type = 2; + optional string last_endpoint_id = 3; +} + +message MediumMetadata { + // True if local device supports 5GHz. + optional bool supports_5_ghz = 1; + // WiFi LAN BSSID, in the form of a six-byte MAC address: XX:XX:XX:XX:XX:XX + optional string bssid = 2; + // IP address, in network byte order: the highest order byte of the address is + // in byte[0]. + optional bytes ip_address = 3; + // True if local device supports 6GHz. + optional bool supports_6_ghz = 4; + // True if local device has mobile radio. + optional bool mobile_radio = 5; + // The frequency of the WiFi LAN AP(in MHz). Or -1 is not associated with an + // AP over WiFi, -2 represents the active network uses an Ethernet transport. + optional int32 ap_frequency = 6 [default = -1]; + // Available channels on the local device. + optional AvailableChannels available_channels = 7; + // Usable WiFi Direct client channels on the local device. + optional WifiDirectCliUsableChannels wifi_direct_cli_usable_channels = 8; + // Usable WiFi LAN channels on the local device. + optional WifiLanUsableChannels wifi_lan_usable_channels = 9; + // Usable WiFi Aware channels on the local device. + optional WifiAwareUsableChannels wifi_aware_usable_channels = 10; + // Usable WiFi Hotspot STA channels on the local device. + optional WifiHotspotStaUsableChannels wifi_hotspot_sta_usable_channels = 11; + // The supported medium roles. + optional MediumRole medium_role = 12; +} + +// Available channels on the local device. +message AvailableChannels { + repeated int32 channels = 1 [packed = true]; +} + +// Usable WiFi Direct client channels on the local device. +message WifiDirectCliUsableChannels { + repeated int32 channels = 1 [packed = true]; +} + +// Usable WiFi LAN channels on the local device. +message WifiLanUsableChannels { + repeated int32 channels = 1 [packed = true]; +} + +// Usable WiFi Aware channels on the local device. +message WifiAwareUsableChannels { + repeated int32 channels = 1 [packed = true]; +} + +// Usable WiFi Hotspot STA channels on the local device. +message WifiHotspotStaUsableChannels { + repeated int32 channels = 1 [packed = true]; +} + +// The medium roles. +message MediumRole { + optional bool support_wifi_direct_group_owner = 1; + optional bool support_wifi_direct_group_client = 2; + optional bool support_wifi_hotspot_host = 3; + optional bool support_wifi_hotspot_client = 4; + optional bool support_wifi_aware_publisher = 5; + optional bool support_wifi_aware_subscriber = 6; + optional bool support_awdl_publisher = 7; + optional bool support_awdl_subscriber = 8; +} + +// LocationHint is used to specify a location as well as format. +message LocationHint { + // Location is the location, provided in the format specified by format. + optional string location = 1; + + // the format of location. + optional LocationStandard.Format format = 2; +} + +message LocationStandard { + enum Format { + UNKNOWN = 0; + // E164 country codes: + // https://en.wikipedia.org/wiki/List_of_country_calling_codes + // e.g. +1 for USA + E164_CALLING = 1; + + // ISO 3166-1 alpha-2 country codes: + // https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 + ISO_3166_1_ALPHA_2 = 2; + } +} + +// Device capability for OS information. +message OsInfo { + enum OsType { + UNKNOWN_OS_TYPE = 0; + ANDROID = 1; + CHROME_OS = 2; + WINDOWS = 3; + APPLE = 4; + LINUX = 100; // g3 test environment + } + + optional OsType type = 1; +} + +enum EndpointType { + UNKNOWN_ENDPOINT = 0; + CONNECTIONS_ENDPOINT = 1; + PRESENCE_ENDPOINT = 2; +} + +message ConnectionsDevice { + optional string endpoint_id = 1; + optional EndpointType endpoint_type = 2; + optional bytes connectivity_info_list = 3; // Data Elements. + optional bytes endpoint_info = 4; +} + +message PresenceDevice { + enum DeviceType { + UNKNOWN = 0; + PHONE = 1; + TABLET = 2; + DISPLAY = 3; + LAPTOP = 4; + TV = 5; + WATCH = 6; + } + + optional string endpoint_id = 1; + optional EndpointType endpoint_type = 2; + optional bytes connectivity_info_list = 3; // Data Elements. + optional int64 device_id = 4; + optional string device_name = 5; + optional DeviceType device_type = 6; + optional string device_image_url = 7; + repeated ConnectionRequestFrame.Medium discovery_medium = 8 [packed = true]; + repeated int32 actions = 9 [packed = true]; + repeated int64 identity_type = 10 [packed = true]; +} diff --git a/play-services-nearby/core/src/main/proto/securegcm.proto b/play-services-nearby/core/src/main/proto/securegcm.proto new file mode 100644 index 0000000000..0325f06e18 --- /dev/null +++ b/play-services-nearby/core/src/main/proto/securegcm.proto @@ -0,0 +1,308 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package securegcm; + +option optimize_for = LITE_RUNTIME; +option java_package = "com.google.security.cryptauth.lib.securegcm"; +option java_outer_classname = "SecureGcmProto"; +option objc_class_prefix = "SGCM"; + +// Message used only during enrollment +// Field numbers should be kept in sync with DeviceInfo in: +// java/com/google/security/cryptauth/backend/services/common/common.proto +message GcmDeviceInfo { + // This field's name does not match the one in DeviceInfo for legacy reasons. + // Consider using long_device_id and device_type instead when enrolling + // non-android devices. + optional fixed64 android_device_id = 1; + + // Used for device_address of DeviceInfo field 2, but for GCM capable devices. + optional bytes gcm_registration_id = 102; + + // Used for device_address of DeviceInfo field 2, but for iOS devices. + optional bytes apn_registration_id = 202; + + // Does the user have notifications enabled for the given device address. + optional bool notification_enabled = 203 [default = true]; + + // Used for device_address of DeviceInfo field 2, a Bluetooth Mac address for + // the device (e.g., to be used with EasyUnlock) + optional string bluetooth_mac_address = 302; + + // SHA-256 hash of the device master key (from the key exchange). + // Differs from DeviceInfo field 3, which contains the actual master key. + optional bytes device_master_key_hash = 103; + + // A SecureMessage.EcP256PublicKey + required bytes user_public_key = 4; + + // device's model name + // (e.g., an android.os.Build.MODEL or UIDevice.model) + optional string device_model = 7; + + // device's locale + optional string locale = 8; + + // The handle for user_public_key (and implicitly, a master key) + optional bytes key_handle = 9; + + // The initial counter value for the device, sent by the device + optional int64 counter = 12 [default = 0]; + + // The Operating System version on the device + // (e.g., an android.os.Build.DISPLAY or UIDevice.systemVersion) + optional string device_os_version = 13; + + // The Operating System version number on the device + // (e.g., an android.os.Build.VERSION.SDK_INT) + optional int64 device_os_version_code = 14; + + // The Operating System release on the device + // (e.g., an android.os.Build.VERSION.RELEASE) + optional string device_os_release = 15; + + // The Operating System codename on the device + // (e.g., an android.os.Build.VERSION.CODENAME or UIDevice.systemName) + optional string device_os_codename = 16; + + // The software version running on the device + // (e.g., Authenticator app version string) + optional string device_software_version = 17; + + // The software version number running on the device + // (e.g., Authenticator app version code) + optional int64 device_software_version_code = 18; + + // Software package information if applicable + // (e.g., com.google.android.apps.authenticator2) + optional string device_software_package = 19; + + // Size of the display in thousandths of an inch (e.g., 7000 mils = 7 in) + optional int32 device_display_diagonal_mils = 22; + + // For Authzen capable devices, their Authzen protocol version + optional int32 device_authzen_version = 24; + + // Not all devices have device identifiers that fit in 64 bits. + optional bytes long_device_id = 29; + + // The device manufacturer name + // (e.g., android.os.Build.MANUFACTURER) + optional string device_manufacturer = 31; + + // Used to indicate which type of device this is. + optional DeviceType device_type = 32 [default = ANDROID]; + + // Fields corresponding to screenlock type/features and hardware features + // should be numbered in the 400 range. + + // Is this device using a secure screenlock (e.g., pattern or pin unlock) + optional bool using_secure_screenlock = 400 [default = false]; + + // Is auto-unlocking the screenlock (e.g., when at "home") supported? + optional bool auto_unlock_screenlock_supported = 401 [default = false]; + + // Is auto-unlocking the screenlock (e.g., when at "home") enabled? + optional bool auto_unlock_screenlock_enabled = 402 [default = false]; + + // Does the device have a Bluetooth (classic) radio? + optional bool bluetooth_radio_supported = 403 [default = false]; + + // Is the Bluetooth (classic) radio on? + optional bool bluetooth_radio_enabled = 404 [default = false]; + + // Does the device hardware support a mobile data connection? + optional bool mobile_data_supported = 405 [default = false]; + + // Does the device support tethering? + optional bool tethering_supported = 406 [default = false]; + + // Does the device have a BLE radio? + optional bool ble_radio_supported = 407 [default = false]; + + // Is the device a "Pixel Experience" Android device? + optional bool pixel_experience = 408 [default = false]; + + // Is the device running in the ARC++ container on a chromebook? + optional bool arc_plus_plus = 409 [default = false]; + + // Is the value set in |using_secure_screenlock| reliable? On some Android + // devices, the platform API to get the screenlock state is not trustworthy. + // See b/32212161. + optional bool is_screenlock_state_flaky = 410 [default = false]; + + // A list of multi-device software features supported by the device. + repeated SoftwareFeature supported_software_features = 411; + + // A list of multi-device software features currently enabled (active) on the + // device. + repeated SoftwareFeature enabled_software_features = 412; + + // The enrollment session id this is sent with + optional bytes enrollment_session_id = 1000; + + // A copy of the user's OAuth token + optional string oauth_token = 1001; +} + +// This enum is used by iOS devices as values for device_display_diagonal_mils +// in GcmDeviceInfo. There is no good way to calculate it on those devices. +enum AppleDeviceDiagonalMils { + // This is the mils diagonal on an iPhone 5. + APPLE_PHONE = 4000; + // This is the mils diagonal on an iPad mini. + APPLE_PAD = 7900; +} + +// This should be kept in sync with DeviceType in: +// java/com/google/security/cryptauth/backend/services/common/common_enums.proto +enum DeviceType { + UNKNOWN = 0; + ANDROID = 1; + CHROME = 2; + IOS = 3; + BROWSER = 4; + OSX = 5; +} + +// MultiDevice features which may be supported and enabled on a device. See +enum SoftwareFeature { + UNKNOWN_FEATURE = 0; + BETTER_TOGETHER_HOST = 1; + BETTER_TOGETHER_CLIENT = 2; + EASY_UNLOCK_HOST = 3; + EASY_UNLOCK_CLIENT = 4; + MAGIC_TETHER_HOST = 5; + MAGIC_TETHER_CLIENT = 6; + SMS_CONNECT_HOST = 7; + SMS_CONNECT_CLIENT = 8; +} + +// A list of "reasons" that can be provided for calling server-side APIs. +// This is particularly important for calls that can be triggered by different +// kinds of events. Please try to keep reasons as generic as possible, so that +// codes can be re-used by various callers in a sensible fashion. +enum InvocationReason { + REASON_UNKNOWN = 0; + // First run of the software package invoking this call + REASON_INITIALIZATION = 1; + // Ordinary periodic actions (e.g. monthly master key rotation) + REASON_PERIODIC = 2; + // Slow-cycle periodic action (e.g. yearly keypair rotation???) + REASON_SLOW_PERIODIC = 3; + // Fast-cycle periodic action (e.g. daily sync for Smart Lock users) + REASON_FAST_PERIODIC = 4; + // Expired state (e.g. expired credentials, or cached entries) was detected + REASON_EXPIRATION = 5; + // An unexpected protocol failure occurred (so attempting to repair state) + REASON_FAILURE_RECOVERY = 6; + // A new account has been added to the device + REASON_NEW_ACCOUNT = 7; + // An existing account on the device has been changed + REASON_CHANGED_ACCOUNT = 8; + // The user toggled the state of a feature (e.g. Smart Lock enabled via BT) + REASON_FEATURE_TOGGLED = 9; + // A "push" from the server caused this action (e.g. a sync tickle) + REASON_SERVER_INITIATED = 10; + // A local address change triggered this (e.g. GCM registration id changed) + REASON_ADDRESS_CHANGE = 11; + // A software update has triggered this + REASON_SOFTWARE_UPDATE = 12; + // A manual action by the user triggered this (e.g. commands sent via adb) + REASON_MANUAL = 13; + // A custom key has been invalidated on the device (e.g. screen lock is + // disabled). + REASON_CUSTOM_KEY_INVALIDATION = 14; + // Periodic action triggered by auth_proximity + REASON_PROXIMITY_PERIODIC = 15; +} + +enum Type { + ENROLLMENT = 0; + TICKLE = 1; + TX_REQUEST = 2; + TX_REPLY = 3; + TX_SYNC_REQUEST = 4; + TX_SYNC_RESPONSE = 5; + TX_PING = 6; + DEVICE_INFO_UPDATE = 7; + TX_CANCEL_REQUEST = 8; + + // DEPRECATED (can be re-used after Aug 2015) + PROXIMITYAUTH_PAIRING = 10; + + // The kind of identity assertion generated by a "GCM V1" device (i.e., + // an Android phone that has registered with us a public and a symmetric + // key) + GCMV1_IDENTITY_ASSERTION = 11; + + // Device-to-device communications are protected by an unauthenticated + // Diffie-Hellman exchange. The InitiatorHello message is simply the + // initiator's public DH key, and is not encoded as a SecureMessage, so + // it doesn't have a tag. + // The ResponderHello message (which is sent by the responder + // to the initiator), on the other hand, carries a payload that is protected + // by the derived shared key. It also contains the responder's + // public DH key. ResponderHelloAndPayload messages have the + // DEVICE_TO_DEVICE_RESPONDER_HELLO tag. + DEVICE_TO_DEVICE_RESPONDER_HELLO_PAYLOAD = 12; + + // Device-to-device communications are protected by an unauthenticated + // Diffie-Hellman exchange. Once the initiator and responder + // agree on a shared key (through Diffie-Hellman), they will use messages + // tagged with DEVICE_TO_DEVICE_MESSAGE to exchange data. + DEVICE_TO_DEVICE_MESSAGE = 13; + + // Notification to let a device know it should contact a nearby device. + DEVICE_PROXIMITY_CALLBACK = 14; + + // Device-to-device communications are protected by an unauthenticated + // Diffie-Hellman exchange. During device-to-device authentication, the first + // message from initiator (the challenge) is signed and put into the payload + // of the message sent back to the initiator. + UNLOCK_KEY_SIGNED_CHALLENGE = 15; + + // Specialty (corp only) features + LOGIN_NOTIFICATION = 101; +} + +message GcmMetadata { + required Type type = 1; + optional int32 version = 2 [default = 0]; +} + +message Tickle { + // Time after which this tickle should expire + optional fixed64 expiry_time = 1; +} + +message LoginNotificationInfo { + // Time at which the server received the login notification request. + optional fixed64 creation_time = 2; + + // Must correspond to user_id in LoginNotificationRequest, if set. + optional string email = 3; + + // Host where the user's credentials were used to login, if meaningful. + optional string host = 4; + + // Location from where the user's credentials were used, if meaningful. + optional string source = 5; + + // Type of login, e.g. ssh, gnome-screensaver, or web. + optional string event_type = 6; +} diff --git a/play-services-nearby/core/src/main/proto/securemessage.proto b/play-services-nearby/core/src/main/proto/securemessage.proto new file mode 100644 index 0000000000..5118d357e3 --- /dev/null +++ b/play-services-nearby/core/src/main/proto/securemessage.proto @@ -0,0 +1,126 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Proto definitions for SecureMessage format + +syntax = "proto2"; + +package securemessage; + +option optimize_for = LITE_RUNTIME; +option java_package = "com.google.security.cryptauth.lib.securemessage"; +option java_outer_classname = "SecureMessageProto"; +option objc_class_prefix = "SMSG"; + +message SecureMessage { + // Must contain a HeaderAndBody message + required bytes header_and_body = 1; + // Signature of header_and_body + required bytes signature = 2; +} + +// Supported "signature" schemes (both symmetric key and public key based) +enum SigScheme { + HMAC_SHA256 = 1; + ECDSA_P256_SHA256 = 2; + // Not recommended -- use ECDSA_P256_SHA256 instead + RSA2048_SHA256 = 3; +} + +// Supported encryption schemes +enum EncScheme { + // No encryption + NONE = 1; + AES_256_CBC = 2; +} + +message Header { + required SigScheme signature_scheme = 1; + required EncScheme encryption_scheme = 2; + // Identifies the verification key + optional bytes verification_key_id = 3; + // Identifies the decryption key + optional bytes decryption_key_id = 4; + // Encryption may use an IV + optional bytes iv = 5; + // Arbitrary per-protocol public data, to be sent with the plain-text header + optional bytes public_metadata = 6; + // The length of some associated data this is not sent in this SecureMessage, + // but which will be bound to the signature. + optional uint32 associated_data_length = 7 [default = 0]; +} + +message HeaderAndBody { + // Public data about this message (to be bound in the signature) + required Header header = 1; + // Payload data + required bytes body = 2; +} + +// Must be kept wire-format compatible with HeaderAndBody. Provides the +// SecureMessage code with a consistent wire-format representation that +// remains stable irrespective of protobuf implementation choices. This +// low-level representation of a HeaderAndBody should not be used by +// any code outside of the SecureMessage library implementation/tests. +message HeaderAndBodyInternal { + // A raw (wire-format) byte encoding of a Header, suitable for hashing + required bytes header = 1; + // Payload data + required bytes body = 2; +} + +// ------- +// The remainder of the messages defined here are provided only for +// convenience. They are not needed for SecureMessage proper, but are +// commonly useful wherever SecureMessage might be applied. +// ------- + +// A list of supported public key types +enum PublicKeyType { + EC_P256 = 1; + RSA2048 = 2; + // 2048-bit MODP group 14, from RFC 3526 + DH2048_MODP = 3; +} + +// A convenience proto for encoding NIST P-256 elliptic curve public keys +message EcP256PublicKey { + // x and y are encoded in big-endian two's complement (slightly wasteful) + // Client MUST verify (x,y) is a valid point on NIST P256 + required bytes x = 1; + required bytes y = 2; +} + +// A convenience proto for encoding RSA public keys with small exponents +message SimpleRsaPublicKey { + // Encoded in big-endian two's complement + required bytes n = 1; + optional int32 e = 2 [default = 65537]; +} + +// A convenience proto for encoding Diffie-Hellman public keys, +// for use only when Elliptic Curve based key exchanges are not possible. +// (Note that the group parameters must be specified separately) +message DhPublicKey { + // Big-endian two's complement encoded group element + required bytes y = 1; +} + +message GenericPublicKey { + required PublicKeyType type = 1; + optional EcP256PublicKey ec_p256_public_key = 2; + optional SimpleRsaPublicKey rsa2048_public_key = 3; + // Use only as a last resort + optional DhPublicKey dh2048_public_key = 4; +} diff --git a/play-services-nearby/core/src/main/proto/sharing_enums.proto b/play-services-nearby/core/src/main/proto/sharing_enums.proto new file mode 100644 index 0000000000..e0e227c3b2 --- /dev/null +++ b/play-services-nearby/core/src/main/proto/sharing_enums.proto @@ -0,0 +1,962 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package location.nearby.proto.sharing; + +// import "logs/proto/logs_annotations/logs_annotations.proto"; +// import "logs/proto/wireless/beto/beto_enums.proto"; +// import "logs/proto/wireless/beto/log_dimension_annotations.proto"; + +// option (logs_proto.file_not_used_for_logging_except_enums) = true; +option optimize_for = LITE_RUNTIME; +option java_package = "com.google.location.nearby.proto"; +option java_outer_classname = "SharingEnums"; +option objc_class_prefix = "GNSHP"; + +// We use event based logging (an event object can be constructed and logged +// immediately when they occur). To obtain session based information (e.g. +// durations, counting incoming introductions), we use flowId (sender/receiver) +// in NearbyClearcutLogger (for android, or clearcut_event_logger as the +// equivalence for Windows) for all events (may exclude settings), and +// session_id for a pair of events (start and end of a session). +// Next id: 74 +enum EventType { + UNKNOWN_EVENT_TYPE = 0; + + // LINT.IfChange(EventType) + + // When new users accept agreements (like grant permission to contacts for + // CONTACT_ONLY visibility) and are enrolled into Nearby Sharing. This event + // is used to count number of new users. + ACCEPT_AGREEMENTS = 1; + + // User enables/disables nearby sharing from setting or tile service. + ENABLE_NEARBY_SHARING = 2; + + // User sets visibility preference from setting. + SET_VISIBILITY = 3; + + // Describe attachments immediately when Nearby Sharing is opened by another + // app which is used to generate/attach attachments to be shared with other + // devices. + DESCRIBE_ATTACHMENTS = 4 + /*[ device_role = DEVICE_ROLE_INITIATOR ]*/; + + // Start of a scanning phase at sender. + SCAN_FOR_SHARE_TARGETS_START = 5 /*[[*/ + /*( device_role = DEVICE_ROLE_INITIATOR, )*/ + /*( stage = STAGE_QS_DISCOVER_START )*/ + /*]]*/; + + // End of the scanning phase at sender. + SCAN_FOR_SHARE_TARGETS_END = 6 /*[[*/ + /*( device_role = DEVICE_ROLE_INITIATOR, )*/ + /*( stage = STAGE_QS_DISCOVER_END )*/ + /*]]*/; + + // Receiver advertises itself for presence (a pseudo session). + ADVERTISE_DEVICE_PRESENCE_START = 7 /*[[*/ + /*( device_role = DEVICE_ROLE_REMOTE, )*/ + /*( stage = STAGE_QS_ADVERTISE_START )*/ + /*]]*/; + + // End of the advertising phase at receiver. + ADVERTISE_DEVICE_PRESENCE_END = 8 /*[[*/ + /*( device_role = DEVICE_ROLE_REMOTE, )*/ + /*( stage = STAGE_QS_ADVERTISE_END )*/ + /*]]*/; + + // Sender sends a fast initialization to receiver. + SEND_FAST_INITIALIZATION = 9 + /*[ device_role = DEVICE_ROLE_INITIATOR ]*/; + + // Receiver receives the fast initialization. + RECEIVE_FAST_INITIALIZATION = 10 + /*[ device_role = DEVICE_ROLE_REMOTE ]*/; + + // Sender discovers a share target. + DISCOVER_SHARE_TARGET = 11 /*[[*/ + /*( device_role = DEVICE_ROLE_INITIATOR, )*/ + /*( stage = STAGE_QS_DISCOVERED )*/ + /*]]*/; + + // Sender sends introduction (before attachments being sent). + SEND_INTRODUCTION = 12 + /*[ device_role = DEVICE_ROLE_INITIATOR ]*/; + + // Receiver receives introduction. + RECEIVE_INTRODUCTION = 13 + /*[ device_role = DEVICE_ROLE_REMOTE ]*/; + + // Receiver responds to introduction (before attachments being sent). + // Actions: Accept, Reject, or (for some reason) Fail. + RESPOND_TO_INTRODUCTION = 14 + /*[ device_role = DEVICE_ROLE_REMOTE ]*/; + + // Start of the sending attachments phase at sender. + SEND_ATTACHMENTS_START = 15 /*[[*/ + /*( device_role = DEVICE_ROLE_INITIATOR, )*/ + /*( stage = STAGE_QS_TRANSFER_START )*/ + /*]]*/; + + // End of sending attachments phase at sender. + SEND_ATTACHMENTS_END = 16 /*[[*/ + /*( device_role = DEVICE_ROLE_INITIATOR, )*/ + /*( stage = STAGE_QS_TRANSFER_END )*/ + /*]]*/; + + // Start of the receiving attachments phase at receiver. + RECEIVE_ATTACHMENTS_START = 17 + /*[ device_role = DEVICE_ROLE_REMOTE ]*/; + + // End of receiving attachments phase at receiver. + RECEIVE_ATTACHMENTS_END = 18 /*[[*/ + /*( device_role = DEVICE_ROLE_REMOTE, )*/ + /*( stage = STAGE_QS_TRANSFER_END )*/ + /*]]*/; + + // Sender cancels sending attachments. + CANCEL_SENDING_ATTACHMENTS = 19 + /*[ device_role = DEVICE_ROLE_INITIATOR ]*/; + + // Receiver cancels receiving attachments. + CANCEL_RECEIVING_ATTACHMENTS = 20 + /*[ device_role = DEVICE_ROLE_REMOTE ]*/; + + // Receiver opens received attachments. + OPEN_RECEIVED_ATTACHMENTS = 21 + /*[ device_role = DEVICE_ROLE_REMOTE ]*/; + + // User opens the setup activity. + LAUNCH_SETUP_ACTIVITY = 22 [deprecated = true]; + + // User adds a contact. + ADD_CONTACT = 23; + + // User removes a contact. + REMOVE_CONTACT = 24; + + // Local devices all Fast Share server. + FAST_SHARE_SERVER_RESPONSE = 25; + + // The start of a sending session. + SEND_START = 26 /*[[*/ + /*( device_role = DEVICE_ROLE_INITIATOR, )*/ + /*( stage = STAGE_QS_CONNECT_START )*/ + /*]]*/; + + // Receiver accepts a fast initialization. + ACCEPT_FAST_INITIALIZATION = 27 + /*[ device_role = DEVICE_ROLE_REMOTE ]*/; + + // Set data usage preference. + SET_DATA_USAGE = 28; + + // Receiver dismisses a fast initialization + DISMISS_FAST_INITIALIZATION = 29 + /*[ device_role = DEVICE_ROLE_REMOTE ]*/; + + // Cancel connection. + CANCEL_CONNECTION = 30 + /*[ device_role = DEVICE_ROLE_INITIATOR ]*/; + + // User starts a chimera activity (e.g. ConsentsChimeraActivity, + // DeviceVisibilityChimeraActivity...) + LAUNCH_ACTIVITY = 31; + + // Receiver dismisses a privacy notification. + DISMISS_PRIVACY_NOTIFICATION = 32; + + // Receiver taps a privacy notification. + TAP_PRIVACY_NOTIFICATION = 33; + + // Receiver taps a help page. + TAP_HELP = 34; + + // Receiver taps a feedback. + TAP_FEEDBACK = 35; + + // Receiver adds quick settings tile. + ADD_QUICK_SETTINGS_TILE = 36; + + // Receiver removes quick settings tile. + REMOVE_QUICK_SETTINGS_TILE = 37; + + // Receiver phone consent clicked. + LAUNCH_PHONE_CONSENT = 38; + + // Devices show a phone consent banner in Nearby Share Settings + DISPLAY_PHONE_CONSENT = 54; + + // Receiver taps quick settings tile. + TAP_QUICK_SETTINGS_TILE = 39; + + // Receiver Installation of APKs status. + INSTALL_APK = 40 /*[ device_role = DEVICE_ROLE_REMOTE ]*/; + + // Receiver verification of APKs status. + VERIFY_APK = 41 /*[ device_role = DEVICE_ROLE_REMOTE ]*/; + + // User starts a consent. + LAUNCH_CONSENT = 42; + + // After receiving payloads, Nearby Share still needs to transfer the payloads + // to correct attachment formats and move files attachments from temporary + // directory to final destination. + PROCESS_RECEIVED_ATTACHMENTS_END = 43 + /*[ device_role = DEVICE_ROLE_REMOTE ]*/; + + // Toggle Show Notification setting item in Nearby Share setting. + TOGGLE_SHOW_NOTIFICATION = 44; + + // Set device name + SET_DEVICE_NAME = 45; + + // users dropped off opt-in page. + DECLINE_AGREEMENTS = 46; + + // Request setting permissions (Wifi/BT/location/airplane mode). + REQUEST_SETTING_PERMISSIONS = 47; + + // Set up a connection with the remote device. + ESTABLISH_CONNECTION = 48 /*[ stage = STAGE_QS_CONNECT_END ]*/; + + // Track device states in Nearby Share setting. + DEVICE_SETTINGS = 49; + + // Receiver auto dismisses a fast initialization notification. + AUTO_DISMISS_FAST_INITIALIZATION = 50 + /*[ device_role = DEVICE_ROLE_REMOTE ]*/; + + // App Crash event. + // Used only for Windows App now. + APP_CRASH = 51; + + // Sender taps the Send button in quick settings + TAP_QUICK_SETTINGS_FILE_SHARE = 52 + /*[ device_role = DEVICE_ROLE_INITIATOR ]*/; + + // Devices show a privacy notification + DISPLAY_PRIVACY_NOTIFICATION = 53; + + // Preference usage event (e.g. load/save preferences, etc). + // Used only for Windows App now. + PREFERENCES_USAGE = 55; + + // Default opt in + DEFAULT_OPT_IN = 56; + + // The result of the setup wizard flow + SETUP_WIZARD = 57; + + // Sender taps a QR code + TAP_QR_CODE = 58 /*[ device_role = DEVICE_ROLE_INITIATOR ]*/; + + // QR code link shown + QR_CODE_LINK_SHOWN = 59 + /*[ device_role = DEVICE_ROLE_INITIATOR ]*/; + + // Sender failed to parse endpoint id. + PARSING_FAILED_ENDPOINT_ID = 60 + /*[ device_role = DEVICE_ROLE_INITIATOR ]*/; + + // The device is discovered by fast initialization + FAST_INIT_DISCOVER_DEVICE = 61 + /*[ device_role = DEVICE_ROLE_REMOTE ]*/; + + // Send desktop notification. + SEND_DESKTOP_NOTIFICATION = 62; + + // User sets account preference + SET_ACCOUNT = 63; + + // Decrypt certificate failure + DECRYPT_CERTIFICATE_FAILURE = 64; + + // Show allow permission auto access UI + SHOW_ALLOW_PERMISSION_AUTO_ACCESS = 65 + /*[ device_role = DEVICE_ROLE_INITIATOR ]*/; + + // UI events for transferring files with desktop applications. It includes + // event types such as DESKTOP_TRANSFER_EVENT_SEND_TYPE_SELECT_A_DEVICE. + SEND_DESKTOP_TRANSFER_EVENT = 66; + + // Show accept button on Quick Share receive UI + WAITING_FOR_ACCEPT = 67; + + // High quality event setup + HIGH_QUALITY_MEDIUM_SETUP = 68; + + // RPC call status + RPC_CALL_STATUS = 69; + + // A QR code sharing session has started + START_QR_CODE_SESSION = 70; + + // A QR code URL has been opened in a web client/browser instead of in a + // native Quick Share app. + QR_CODE_OPENED_IN_WEB_CLIENT = 71; + + // A HaTS survey session id has been joined with Quick Share flow id. + HATS_JOINT_EVENT = 72; + + // Previews received. + RECEIVE_PREVIEWS = 73; + + // LINT.ThenChange(//depot/google3/location/nearby/proto/nearby_event_codes.proto:SharingEventCode) +} + +// Event category to differentiate whether this comes from sender or receiver, +// whether this is for communication flow, or for settings. +enum EventCategory { + UNKNOWN_EVENT_CATEGORY = 0; + SENDING_EVENT = 1; + RECEIVING_EVENT = 2; + SETTINGS_EVENT = 3; + RPC_EVENT = 4; +} + +// Status of nearby sharing. +enum NearbySharingStatus { + UNKNOWN_NEARBY_SHARING_STATUS = 0; + + ON = 1; + OFF = 2; +} + +enum Visibility { + UNKNOWN_VISIBILITY = 0; + + CONTACTS_ONLY = 1; + EVERYONE = 2; + SELECTED_CONTACTS_ONLY = 3 [deprecated = true]; + HIDDEN = 4; + SELF_SHARE = 5; +} + +enum DataUsage { + UNKNOWN_DATA_USAGE = 0; + + ONLINE = 1; + WIFI_ONLY = 2; + OFFLINE = 3; +} + +// The status of establishing a connection. Used by ESTABLISH_CONNECTION. +enum EstablishConnectionStatus { + CONNECTION_STATUS_UNKNOWN = 0; + + CONNECTION_STATUS_SUCCESS = 1 + /*[ status_bucket = STATUS_SUCCESSFUL ]*/; + CONNECTION_STATUS_FAILURE = 2 + /*[ status_bucket = STATUS_INTERNAL_ERROR ]*/; + CONNECTION_STATUS_CANCELLATION = 3 + /*[ status_bucket = STATUS_CANCELLED ]*/; + CONNECTION_STATUS_MEDIA_UNAVAILABLE_ATTACHMENT = 4 + /*[ status_bucket = STATUS_INTERNAL_ERROR ]*/; + CONNECTION_STATUS_FAILED_PAIRED_KEYHANDSHAKE = 5 + /*[ status_bucket = STATUS_INTERNAL_ERROR ]*/; + CONNECTION_STATUS_FAILED_WRITE_INTRODUCTION = 6 + /*[ status_bucket = STATUS_INTERNAL_ERROR ]*/; + CONNECTION_STATUS_FAILED_NULL_CONNECTION = 7 + /*[ status_bucket = STATUS_INTERNAL_ERROR ]*/; + CONNECTION_STATUS_FAILED_NO_TRANSFER_UPDATE_CALLBACK = 8 + /*[ status_bucket = STATUS_INTERNAL_ERROR ]*/; + CONNECTION_STATUS_LOST_CONNECTIVITY = 9 + /*[ status_bucket = STATUS_INTERRUPTION ]*/; + // TODO: b/341782941 - : Annote this status when it's confirmed by Nearby + // Connections team. + CONNECTION_STATUS_INVALID_ADVERTISEMENT = 10; +} + +// The status of sending and receiving attachments. Used by SEND_ATTACHMENTS. +enum AttachmentTransmissionStatus { + UNKNOWN_ATTACHMENT_TRANSMISSION_STATUS = 0; + COMPLETE_ATTACHMENT_TRANSMISSION_STATUS = 1 + /*[ status_bucket = STATUS_SUCCESSFUL ]*/; + CANCELED_ATTACHMENT_TRANSMISSION_STATUS = 2 + /*[ status_bucket = STATUS_CANCELLED ]*/; + FAILED_ATTACHMENT_TRANSMISSION_STATUS = 3 + /*[ status_bucket = STATUS_INTERNAL_ERROR ]*/; + REJECTED_ATTACHMENT = 4 [deprecated = true]; + TIMED_OUT_ATTACHMENT = 5 [deprecated = true]; + AWAITING_REMOTE_ACCEPTANCE_FAILED_ATTACHMENT = 6 [deprecated = true]; + NOT_ENOUGH_SPACE_ATTACHMENT = 7 [deprecated = true]; + FAILED_NO_TRANSFER_UPDATE_CALLBACK = 8 [deprecated = true]; + MEDIA_UNAVAILABLE_ATTACHMENT = 9 [deprecated = true]; + UNSUPPORTED_ATTACHMENT_TYPE_ATTACHMENT = 10 [deprecated = true]; + NO_ATTACHMENT_FOUND = 11 [deprecated = true]; + FAILED_NO_SHARE_TARGET_ENDPOINT = 12 [deprecated = true]; + FAILED_PAIRED_KEYHANDSHAKE = 13 [deprecated = true]; + FAILED_NULL_CONNECTION = 14 [deprecated = true]; + FAILED_NO_PAYLOAD = 15 [deprecated = true]; + FAILED_WRITE_INTRODUCTION = 16 [deprecated = true]; + + // The remote response is either missing or has an unknown type. + FAILED_UNKNOWN_REMOTE_RESPONSE = 17 [deprecated = true]; + + // Breakdowns of FAILED_NULL_CONNECTION (Desktop side) + FAILED_NULL_CONNECTION_INIT_OUTGOING = 18; + FAILED_NULL_CONNECTION_DISCONNECTED = 19; + + // Breakdowns of FAILED_NULL_CONNECTION (android side) + // Connection failed due to Wifi is disconnected or Bluetooth setting is off + // or user turn on airplane mode. + FAILED_NULL_CONNECTION_LOST_CONNECTIVITY = 20 [deprecated = true]; + // Unexpected connection failure. + FAILED_NULL_CONNECTION_FAILURE = 21 [deprecated = true]; + + REJECTED_ATTACHMENT_TRANSMISSION_STATUS = 22; + TIMED_OUT_ATTACHMENT_TRANSMISSION_STATUS = 23; + NOT_ENOUGH_SPACE_ATTACHMENT_TRANSMISSION_STATUS = 24 + /*[ status_bucket = STATUS_INTERRUPTION ]*/; + UNSUPPORTED_ATTACHMENT_TYPE_ATTACHMENT_TRANSMISSION_STATUS = 25; + FAILED_UNKNOWN_REMOTE_RESPONSE_TRANSMISSION_STATUS = 26; + // Connection failed due to Wifi is disconnected or Bluetooth setting is off + // or user turn on airplane mode. + NO_RESPONSE_FRAME_CONNECTION_CLOSED_LOST_CONNECTIVITY_TRANSMISSION_STATUS = 27 + [deprecated = true]; + // Unexpected connection failure due to no response frame. + NO_RESPONSE_FRAME_CONNECTION_CLOSED_TRANSMISSION_STATUS = 28; + // Connection failed due to Wifi is disconnected or Bluetooth setting is off + // or user turn on airplane mode. + LOST_CONNECTIVITY_TRANSMISSION_STATUS = 29 + /*[ status_bucket = STATUS_INTERRUPTION ]*/; + // Connection failed due to the medium is not allowed. + FAILED_DISALLOWED_MEDIUM = 30; +} + +// Generic result status of NearbyConnections API calls. +enum ConnectionLayerStatus { + // No status is available + CONNECTION_LAYER_STATUS_UNKNOWN = 0; + // The operation was successful. + CONNECTION_LAYER_STATUS_SUCCESS = 1; + // The operation failed, without any more information. + CONNECTION_LAYER_STATUS_ERROR = 2; + // The app called an API method out of order (i.e. another method is expected + // to be called first). + CONNECTION_LAYER_STATUS_OUT_OF_ORDER_API_CALL = 3; + // The app already has active operations (advertising, discovering, or + // connected to other devices) with another Strategy. Stop these operations on + // the current Strategy before trying to advertise or discover with a new + // Strategy. + CONNECTION_LAYER_STATUS_ALREADY_HAVE_ACTIVE_STRATEGY = 4; + // The app is already advertising; call StopAdvertising() before trying to + // advertise again. + CONNECTION_LAYER_STATUS_ALREADY_ADVERTISING = 5; + // The app is already discovering; call StopDiscovery() before trying to + // discover again. + CONNECTION_LAYER_STATUS_ALREADY_DISCOVERING = 6; + // NC is already listening for incoming connections from remote endpoints. + CONNECTION_LAYER_STATUS_ALREADY_LISTENING = 7; + // An attempt to read from/write to a connected remote endpoint failed. If + // this occurs repeatedly, consider invoking DisconnectFromEndpoint(). + CONNECTION_LAYER_STATUS_END_POINT_IO_ERROR = 8; + // An attempt to interact with a remote endpoint failed because it's unknown + // to us -- it's either an endpoint that was never discovered, or an endpoint + // that never connected to us (both of which are indicative of bad input from + // the client app). + CONNECTION_LAYER_STATUS_END_POINT_UNKNOWN = 9; + // The remote endpoint rejected the connection request. + CONNECTION_LAYER_STATUS_CONNECTION_REJECTED = 10; + // The app is already connected to the specified endpoint. Multiple + // connections to a remote endpoint cannot be maintained simultaneously. + CONNECTION_LAYER_STATUS_ALREADY_CONNECTED_TO_END_POINT = 11; + // The remote endpoint is not connected; messages cannot be sent to it. + CONNECTION_LAYER_STATUS_NOT_CONNECTED_TO_END_POINT = 12; + // There was an error trying to use the device's Bluetooth capabilities. + CONNECTION_LAYER_STATUS_BLUETOOTH_ERROR = 13; + // There was an error trying to use the device's Bluetooth Low Energy + // capabilities. + CONNECTION_LAYER_STATUS_BLE_ERROR = 14; + // There was an error trying to use the device's Wi-Fi capabilities. + CONNECTION_LAYER_STATUS_WIFI_LAN_ERROR = 15; + // An attempt to interact with an in-flight Payload failed because it's + // unknown to us. + CONNECTION_LAYER_STATUS_PAYLOAD_UNKNOWN = 16; + // The connection was reset + CONNECTION_LAYER_STATUS_RESET = 17; + // The connection timed out + CONNECTION_LAYER_STATUS_TIMEOUT = 18; +} + +// The status of processing attachments after receiver received payloads +// successfully. +enum ProcessReceivedAttachmentsStatus { + PROCESSING_STATUS_UNKNOWN = 0; + + PROCESSING_STATUS_COMPLETE_PROCESSING_ATTACHMENTS = 1; + PROCESSING_STATUS_FAILED_MOVING_FILES = 2; + PROCESSING_STATUS_FAILED_RECEIVING_APK = 3; + PROCESSING_STATUS_FAILED_RECEIVING_TEXT = 4; + PROCESSING_STATUS_FAILED_RECEIVING_WIFI_CREDENTIALS = 5; +} + +// The status of advertising and discovering sessions. Used by +// SCAN_FOR_SHARE_TARGETS and ADVERTISE_DEVICE_PRESENCE. +enum SessionStatus { + UNKNOWN_SESSION_STATUS = 0; + SUCCEEDED_SESSION_STATUS = 1 + /*[ status_bucket = STATUS_SUCCESSFUL ]*/; + + // TODO: b/341782941 - FAILED_SESSION_STATUS occurs when the status of + // advertising or discovering sessions is not successful. It can be + // due to STATUS_INTERNAL_ERROR, STATUS_INTERRUPTION, STATUS_CANCELLED. + // More session statuses should be logged to determine the status. + FAILED_SESSION_STATUS = 2; +} + +// User's response to introductions. +enum ResponseToIntroduction { + UNKNOWN_RESPONSE_TO_INTRODUCTION = 0; + + ACCEPT_INTRODUCTION = 1; + REJECT_INTRODUCTION = 2; + FAIL_INTRODUCTION = 3; +} + +// TODO(fdi): may eventually include desktop, etc. +// The type of a remote device. +enum DeviceType { + UNKNOWN_DEVICE_TYPE = 0; + + PHONE = 1; + TABLET = 2; + LAPTOP = 3; + CAR = 4; + FOLDABLE = 5; + XR = 6; +} + +// TODO(fdi): may eventually include windows, iOS, etc. +// The OS type of a remote device. +enum OSType { + UNKNOWN_OS_TYPE = 0; + + ANDROID = 1; + CHROME_OS = 2; + IOS = 3; + WINDOWS = 4; + MACOS = 5; +} + +// Relationship of remote device to sender device. +enum DeviceRelationship { + UNKNOWN_DEVICE_RELATIONSHIP = 0; + + // The remote device belongs to the same owner as sender device. + IS_SELF = 1; + // The remote device is a contact of sender. + IS_CONTACT = 2; + // The remote device is a stranger. + IS_STRANGER = 3; +} + +// The device sources of the clearcut log. +enum LogSource { + UNSPECIFIED_SOURCE = 0; + + // Represents the devices in Nearby labs. + LAB_DEVICES = 1; + // Represents the devices tested by Nearby engs, in the long term can include + // any devices with newest feature flags. + INTERNAL_DEVICES = 2; + // Represents the devices testing our in-development features before they're + // released to the greater public. + BETA_TESTER_DEVICES = 3; + // Represents the OEM partners (like Samsung) that we're working with to + // verify functionality on their devices. + OEM_DEVICES = 4; + // Represents the device for debugging. + DEBUG_DEVICES = 5; + // Represents the device for Nearby Module Food. + NEARBY_MODULE_FOOD_DEVICES = 6; + // Represents the device for BeTo Team Food. + BETO_DOGFOOD_DEVICES = 7; + // Represents the device for Nearby dog Food. + NEARBY_DOGFOOD_DEVICES = 8; + // Represents the device for Nearby Team Food. + NEARBY_TEAMFOOD_DEVICES = 9; +} + +// The Fast Share server action name. +enum ServerActionName { + UNKNOWN_SERVER_ACTION = 0; + + UPLOAD_CERTIFICATES = 1; + DOWNLOAD_CERTIFICATES = 2; + CHECK_REACHABILITY = 3; + UPLOAD_CONTACTS = 4; + UPDATE_DEVICE_NAME = 5; + UPLOAD_SENDER_CERTIFICATES = 6; + DOWNLOAD_SENDER_CERTIFICATES = 7; + UPLOAD_CONTACTS_AND_CERTIFICATES = 8; + LIST_REACHABLE_PHONE_NUMBERS = 9; + LIST_MY_DEVICES = 10; + LIST_CONTACT_PEOPLE = 11; + + // used for analytics logger to record action name. + DOWNLOAD_CERTIFICATES_INFO = 12; +} + +// The Fast Share server response state. +enum ServerResponseState { + UNKNOWN_SERVER_RESPONSE_STATE = 0; + + SERVER_RESPONSE_SUCCESS = 1; + SERVER_RESPONSE_UNKNOWN_FAILURE = 2; + + // For StatusException. + SERVER_RESPONSE_STATUS_OTHER_FAILURE = 3; + SERVER_RESPONSE_STATUS_DEADLINE_EXCEEDED = 4; + SERVER_RESPONSE_STATUS_PERMISSION_DENIED = 5; + SERVER_RESPONSE_STATUS_UNAVAILABLE = 6; + SERVER_RESPONSE_STATUS_UNAUTHENTICATED = 7; + SERVER_RESPONSE_STATUS_INVALID_ARGUMENT = 9; + + // For GoogleAuthException. + SERVER_RESPONSE_GOOGLE_AUTH_FAILURE = 8; + + // For Internet connect status. + SERVER_RESPONSE_NOT_CONNECTED_TO_INTERNET = 10; +} + +// The purpose of requesting the server request. +enum SyncPurpose { + SYNC_PURPOSE_UNKNOWN = 0; + // When NearbySharingChimeraService#sync() is called. + SYNC_PURPOSE_ON_DEMAND_SYNC = 1; + // Requested by chime notification. + SYNC_PURPOSE_CHIME_NOTIFICATION = 2; + // For reqular daily sync. + SYNC_PURPOSE_DAILY_SYNC = 3; + // Wen a device opts into Nearby Share. + SYNC_PURPOSE_OPT_IN_FIRST_SYNC = 4; + // Requested when Nearby Share automatically enables a device that shares + // a single account that has already opted in on another device. + SYNC_PURPOSE_CHECK_DEFAULT_OPT_IN = 5; + // When a device enables Nearby Share. + SYNC_PURPOSE_NEARBY_SHARE_ENABLED = 6; + // When a device is in fast init advertising. + SYNC_PURPOSE_SYNC_AT_FAST_INIT = 7; + // When device start discovery. + SYNC_PURPOSE_SYNC_AT_DISCOVERY = 8; + // When device tries to load valid private certificate. + SYNC_PURPOSE_SYNC_AT_LOAD_PRIVATE_CERTIFICATE = 9; + // When device start advertiseement. + SYNC_PURPOSE_SYNC_AT_ADVERTISEMENT = 10; + // When device contacts list changes. + SYNC_PURPOSE_CONTACT_LIST_CHANGE = 11; + // When showing the C11 banner in Neary Share setting. + SYNC_PURPOSE_SHOW_C11N_VIEW = 12; + // For regular check contact reachability. + SYNC_PURPOSE_REGULAR_CHECK_CONTACT_REACHABILITY = 13; + // When selected contacts list changes in visibility setting. + SYNC_PURPOSE_VISIBILITY_SELECTED_CONTACT_CHANGE = 14; + // When switching account. + SYNC_PURPOSE_ACCOUNT_CHANGE = 15; + // When regenerate certificates + SYNC_PURPOSE_REGENERATE_CERTIFICATES = 16; + // When Device Contacts consent changes + SYNC_PURPOSE_DEVICE_CONTACTS_CONSENT_CHANGE = 17; +} + +// The device role to trigger the server request. +enum ClientRole { + CLIENT_ROLE_UNKNOWN = 0; + CLIENT_ROLE_SENDER = 1; + CLIENT_ROLE_RECEIVER = 2; +} + +// The type of Nearby Sharing scanning. +enum ScanType { + UNKNOWN_SCAN_TYPE = 0; + FOREGROUND_SCAN = 1; + FOREGROUND_RETRY_SCAN = 2; + DIRECT_SHARE_SCAN = 3; + BACKGROUND_SCAN = 4; +} + +// The type of parsing endpoint id failed type. +enum ParsingFailedType { + FAILED_UNKNOWN_TYPE = 0; + // NULL advertisement is returned due to sender failing to parse advertisement + // from endpointInfo byte stream from receiver advertisement. + FAILED_PARSE_ADVERTISEMENT = 1; + // NULL shareTarget is returned due to sender failing to create shareTarget + // from a valid parsed advertisement stemming from issues in certificates, QR + // code tokens or device names. + FAILED_CONVERT_SHARE_TARGET = 2; +} + +// The Nearby Sharing advertising mode. +enum AdvertisingMode { + UNKNOWN_ADVERTISING_MODE = 0; + SCREEN_OFF_ADVERTISING_MODE = 1; + BACKGROUND_ADVERTISING_MODE = 2; + MIDGROUND_ADVERTISING_MODE = 3; + FOREGROUND_ADVERTISING_MODE = 4; + SUSPENDED_ADVERTISING_MODE = 5; +} + +// The Nearby Sharing discovery mode. +enum DiscoveryMode { + UNKNOWN_DISCOVERY_MODE = 0; + SCREEN_OFF_DISCOVERY_MODE = 1; + BACKGROUND_DISCOVERY_MODE = 2; + MIDGROUND_DISCOVERY_MODE = 3; + FOREGROUND_DISCOVERY_MODE = 4; + SUSPENDED_DISCOVERY_MODE = 5; +} + +// The class name of chimera activity. +enum ActivityName { + UNKNOWN_ACTIVITY = 0; + + SHARE_SHEET_ACTIVITY = 1; + SETTINGS_ACTIVITY = 2; + RECEIVE_SURFACE_ACTIVITY = 3; + SETUP_ACTIVITY = 4; + DEVICE_VISIBILITY_ACTIVITY = 5; + CONSENTS_ACTIVITY = 6; + SET_DEVICE_NAME_DIALOG = 7; + SET_DATA_USAGE_DIALOG = 8; + QUICK_SETTINGS_ACTIVITY = 9; + REMOTE_COPY_SHARE_SHEET_ACTIVITY = 10; + SETUP_WIZARD_ACTIVITY = 11; + SETTINGS_REVIEW_ACTIVITY = 12; +} + +// The type of consent page user launches. +enum ConsentType { + CONSENT_TYPE_UNKNOWN = 0; + + // launch Constellation consent page. + CONSENT_TYPE_C11N = 1; + // launch device contact consent page. + CONSENT_TYPE_DEVICE_CONTACT = 2; +} + +enum ConsentAcceptanceStatus { + CONSENT_UNKNOWN_ACCEPT_STATUS = 0; + + CONSENT_ACCEPTED = 1; + CONSENT_DECLINED = 2; + // unable to enable consent. + CONSENT_UNABLE_TO_ENABLE = 3; +} + +enum ApkSource { + UNKNOWN_APK_SOURCE = 0; + + APK_FROM_SD_CARD = 1; + INSTALLED_APP = 2; +} + +// The Installation status of APK. +enum InstallAPKStatus { + UNKNOWN_INSTALL_APK_STATUS = 0; + + FAIL_INSTALLATION = 1; + SUCCESS_INSTALLATION = 2; +} + +// The verification status of APK. +enum VerifyAPKStatus { + UNKNOWN_VERIFY_APK_STATUS = 0; + + NOT_INSTALLABLE = 1; + INSTALLABLE = 2; + ALREADY_INSTALLED = 3; +} + +enum ShowNotificationStatus { + UNKNOWN_SHOW_NOTIFICATION_STATUS = 0; + + SHOW = 1; + NOT_SHOW = 2; +} + +enum PermissionRequestResult { + PERMISSION_UNKNOWN_REQUEST_RESULT = 0; + + PERMISSION_GRANTED = 1; + PERMISSION_REJECTED = 2; + PERMISSION_UNABLE_TO_GRANT = 3; +} + +enum PermissionRequestType { + PERMISSION_UNKNOWN_TYPE = 0; + + PERMISSION_AIRPLANE_MODE_OFF = 1; + PERMISSION_WIFI = 2; + PERMISSION_BLUETOOTH = 3; + PERMISSION_LOCATION = 4; + PERMISSION_WIFI_HOTSPOT = 5; +} + +enum SharingUseCase { + USE_CASE_UNKNOWN = 0; + + USE_CASE_NEARBY_SHARE = 1; + USE_CASE_REMOTE_COPY_PASTE = 2; + USE_CASE_WIFI_CREDENTIAL = 3; + USE_CASE_APP_SHARE = 4; + USE_CASE_QUICK_SETTING_FILE_SHARE = 5; + USE_CASE_SETUP_WIZARD = 6; + // Deprecated. QR code is an addition to existing use cases rather than being + // a separate use case. + USE_CASE_NEARBY_SHARE_WITH_QR_CODE = 7 [deprecated = true]; + // The user was redirected from Bluetooth sharing UI to Nearby Share + USE_CASE_REDIRECTED_FROM_BLUETOOTH_SHARE = 8; +} + +// Used only for Windows App now. +enum AppCrashReason { + APP_CRASH_REASON_UNKNOWN = 0; +} + +// Thes source where the attachemnt comes from. It can be an action, app name, +// etc. The first 6 source types are being used as FileSenderType in Nearby +// Share Windows app. +enum AttachmentSourceType { + ATTACHMENT_SOURCE_UNKNOWN = 0; + ATTACHMENT_SOURCE_CONTEXT_MENU = 1; + ATTACHMENT_SOURCE_DRAG_AND_DROP = 2; + ATTACHMENT_SOURCE_SELECT_FILES_BUTTON = 3; + ATTACHMENT_SOURCE_PASTE = 4; + ATTACHMENT_SOURCE_SELECT_FOLDERS_BUTTON = 5; + ATTACHMENT_SOURCE_SHARE_ACTIVATION = 6; +} + +// The action to interact with preferences. +// Used only for Windows App now. +enum PreferencesAction { + PREFERENCES_ACTION_UNKNOWN = 0; + PREFERENCES_ACTION_NO_ACTION = 1; + + // Primary actions/functions towards preferences + PREFERENCES_ACTION_LOAD_PREFERENCES = 2; + PREFERENCES_ACTION_SAVE_PREFERENCESS = 3; + PREFERENCES_ACTION_ATTEMPT_LOAD = 4; + PREFERENCES_ACTION_RESTORE_FROM_BACKUP = 5; + + // Other actions within the 4 actions above + PREFERENCES_ACTION_CREATE_PREFERENCES_PATH = 6; + PREFERENCES_ACTION_MAKE_PREFERENCES_BACKUP_FILE = 7; + PREFERENCES_ACTION_CHECK_IF_PREFERENCES_PATH_EXISTS = 8; + PREFERENCES_ACTION_CHECK_IF_PREFERENCES_INPUT_STREAM_STATUS = 9; + PREFERENCES_ACTION_CHECK_IF_PREFERENCES_FILE_IS_CORRUPTED = 10; + PREFERENCES_ACTION_CHECK_IF_PREFERENCES_BACKUP_FILE_EXISTS = 11; +} + +// The status of the action to interact with preferences. +// Used only for Windows App now. +enum PreferencesActionStatus { + PREFERENCES_ACTION_STATUS_UNKNOWN = 0; + PREFERENCES_ACTION_STATUS_SUCCESS = 1; + PREFERENCES_ACTION_STATUS_FAIL = 2; +} + +/** The distance of the found nearby fast init advertisement. */ +enum FastInitState { + FAST_INIT_UNKNOWN_STATE = 0; + // A device was found in close proximity. + // distance < fast_init_distance_close_centimeters(50 cm) + FAST_INIT_CLOSE_STATE = 1; + // A device was found in far proximity. + // distance < fast_init_distance_close_centimeters(10 m) + FAST_INIT_FAR_STATE = 2; + // No devices have been found nearby. The default state. + FAST_INIT_LOST_STATE = 3; +} + +/** The type of FastInit advertisement. */ +enum FastInitType { + FAST_INIT_UNKNOWN_TYPE = 0; + // Show HUN to notify the user. + FAST_INIT_NOTIFY_TYPE = 1; + // Not notify the user. + FAST_INIT_SILENT_TYPE = 2; +} + +// LINT.IfChange +/** The type of desktop notification event. */ +enum DesktopNotification { + DESKTOP_NOTIFICATION_UNKNOWN = 0; + DESKTOP_NOTIFICATION_CONNECTING = 1; + DESKTOP_NOTIFICATION_PROGRESS = 2; + DESKTOP_NOTIFICATION_ACCEPT = 3; + DESKTOP_NOTIFICATION_RECEIVED = 4; + DESKTOP_NOTIFICATION_ERROR = 5; +} + +enum DesktopTransferEventType { + DESKTOP_TRANSFER_EVENT_TYPE_UNKNOWN = 0; + + // Receive attachments. + DESKTOP_TRANSFER_EVENT_RECEIVE_TYPE_ACCEPT = 1; + DESKTOP_TRANSFER_EVENT_RECEIVE_TYPE_PROGRESS = 2; + DESKTOP_TRANSFER_EVENT_RECEIVE_TYPE_RECEIVED = 3; + DESKTOP_TRANSFER_EVENT_RECEIVE_TYPE_ERROR = 4; + + // Send attachments. + DESKTOP_TRANSFER_EVENT_SEND_TYPE_START = 5; + DESKTOP_TRANSFER_EVENT_SEND_TYPE_SELECT_A_DEVICE = 6; + DESKTOP_TRANSFER_EVENT_SEND_TYPE_PROGRESS = 7; + DESKTOP_TRANSFER_EVENT_SEND_TYPE_SENT = 8; + DESKTOP_TRANSFER_EVENT_SEND_TYPE_ERROR = 9; +} + +enum DecryptCertificateFailureStatus { + DECRYPT_CERT_UNKNOWN_FAILURE = 0; + DECRYPT_CERT_NO_SUCH_ALGORITHM_FAILURE = 1; + DECRYPT_CERT_NO_SUCH_PADDING_FAILURE = 2; + DECRYPT_CERT_INVALID_KEY_FAILURE = 3; + DECRYPT_CERT_INVALID_ALGORITHM_PARAMETER_FAILURE = 4; + DECRYPT_CERT_ILLEGAL_BLOCK_SIZE_FAILURE = 5; + DECRYPT_CERT_BAD_PADDING_FAILURE = 6; +} + +// Refer to go/qs-contacts-consent-2024 for the detail. +enum ContactAccess { + CONTACT_ACCESS_UNKNOWN = 0; + + CONTACT_ACCESS_NO_CONTACT_UPLOADED = 1; + CONTACT_ACCESS_ONLY_UPLOAD_GOOGLE_CONTACT = 2; + CONTACT_ACCESS_UPLOAD_CONTACT_FOR_DEVICE_CONTACT_CONSENT = 3; + CONTACT_ACCESS_UPLOAD_CONTACT_FOR_QUICK_SHARE_CONSENT = 4; +} + +// Refer to go/qs-contacts-consent-2024 for the detail. +enum IdentityVerification { + IDENTITY_VERIFICATION_UNKNOWN = 0; + + IDENTITY_VERIFICATION_NO_PHONE_NUMBER_VERIFIED = 1; + IDENTITY_VERIFICATION_PHONE_NUMBER_VERIFIED_NOT_LINKED_TO_GAIA = 2; + IDENTITY_VERIFICATION_PHONE_NUMBER_VERIFIED_LINKED_TO_QS_GAIA = 3; +} + +enum ButtonStatus { + BUTTON_STATUS_UNKNOWN = 0; + BUTTON_STATUS_CLICK_ACCEPT = 1; + BUTTON_STATUS_CLICK_REJECT = 2; + BUTTON_STATUS_IGNORE = 3; +} +// LINT.ThenChange( +// //depot/google3/location/nearby/cpp/sharing/clients/dart/platform/lib/types/models.dart +// ) diff --git a/play-services-nearby/core/src/main/proto/ukey.proto b/play-services-nearby/core/src/main/proto/ukey.proto new file mode 100644 index 0000000000..327d8d3d37 --- /dev/null +++ b/play-services-nearby/core/src/main/proto/ukey.proto @@ -0,0 +1,105 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package securegcm; + +option optimize_for = LITE_RUNTIME; +option java_package = "com.google.security.cryptauth.lib.securegcm"; +option java_outer_classname = "UkeyProto"; + +message Ukey2Message { + enum Type { + UNKNOWN_DO_NOT_USE = 0; + ALERT = 1; + CLIENT_INIT = 2; + SERVER_INIT = 3; + CLIENT_FINISH = 4; + } + + optional Type message_type = 1; // Identifies message type + optional bytes message_data = 2; // Actual message, to be parsed according to + // message_type +} + +message Ukey2Alert { + enum AlertType { + // Framing errors + BAD_MESSAGE = 1; // The message could not be deserialized + BAD_MESSAGE_TYPE = 2; // message_type has an undefined value + INCORRECT_MESSAGE = 3; // message_type received does not correspond to + // expected type at this stage of the protocol + BAD_MESSAGE_DATA = 4; // Could not deserialize message_data as per + // value inmessage_type + + // ClientInit and ServerInit errors + BAD_VERSION = 100; // version is invalid; server cannot find + // suitable version to speak with client. + BAD_RANDOM = 101; // Random data is missing or of incorrect + // length + BAD_HANDSHAKE_CIPHER = 102; // No suitable handshake ciphers were found + BAD_NEXT_PROTOCOL = 103; // The next protocol is missing, unknown, or + // unsupported + BAD_PUBLIC_KEY = 104; // The public key could not be parsed + + // Other errors + INTERNAL_ERROR = 200; // An internal error has occurred. error_message + // may contain additional details for logging + // and debugging. + } + + optional AlertType type = 1; + optional string error_message = 2; +} + +enum Ukey2HandshakeCipher { + RESERVED = 0; + P256_SHA512 = 100; // NIST P-256 used for ECDH, SHA512 used for + // commitment + CURVE25519_SHA512 = 200; // Curve 25519 used for ECDH, SHA512 used for + // commitment +} + +message Ukey2ClientInit { + optional int32 version = 1; // highest supported version for rollback + // protection + optional bytes random = 2; // random bytes for replay/reuse protection + + // One commitment (hash of ClientFinished containing public key) per supported + // cipher + message CipherCommitment { + optional Ukey2HandshakeCipher handshake_cipher = 1; + optional bytes commitment = 2; + } + repeated CipherCommitment cipher_commitments = 3; + + // Next protocol that the client wants to speak. + optional string next_protocol = 4; +} + +message Ukey2ServerInit { + optional int32 version = 1; // highest supported version for rollback + // protection + optional bytes random = 2; // random bytes for replay/reuse protection + + // Selected Cipher and corresponding public key + optional Ukey2HandshakeCipher handshake_cipher = 3; + optional bytes public_key = 4; +} + +message Ukey2ClientFinished { + optional bytes public_key = 1; // public key matching selected handshake + // cipher +} diff --git a/play-services-nearby/core/src/main/proto/wire_format.proto b/play-services-nearby/core/src/main/proto/wire_format.proto new file mode 100644 index 0000000000..82510d303c --- /dev/null +++ b/play-services-nearby/core/src/main/proto/wire_format.proto @@ -0,0 +1,408 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +//package nearby.sharing.service.proto; +package sharing.nearby; + +// import "storage/datapol/annotations/proto/semantic_annotations.proto"; +import "sharing_enums.proto"; + +option java_package = "com.google.android.gms.nearby.sharing"; +option java_outer_classname = "Protocol"; +option objc_class_prefix = "GNSHP"; +option optimize_for = LITE_RUNTIME; + +// File metadata. Does not include the actual bytes of the file. +// NEXT_ID=10 +message FileMetadata { + enum Type { + UNKNOWN = 0; + IMAGE = 1; + VIDEO = 2; + ANDROID_APP = 3; + AUDIO = 4; + DOCUMENT = 5; + CONTACT_CARD = 6; + } + + // The human readable name of this file (eg. 'Cookbook.pdf'). + optional string name = 1; + + // The type of file (eg. 'IMAGE' from 'dog.jpg'). Specifying a type helps + // provide a richer experience on the receiving side. + optional Type type = 2 [default = UNKNOWN]; + + // The FILE payload id that will be sent as a follow up containing the actual + // bytes of the file. + optional int64 payload_id = 3; + + // The total size of the file. + optional int64 size = 4; + + // The mimeType of file (eg. 'image/jpeg' from 'dog.jpg'). Specifying a + // mimeType helps provide a richer experience on receiving side. + optional string mime_type = 5 [default = "application/octet-stream"]; + + // A uuid for the attachment. Should be unique across all attachments. + optional int64 id = 6; + + // The parent folder. + optional string parent_folder = 7; + + // A stable identifier for the attachment. Used for receiver to identify same + // attachment from different transfers. + optional int64 attachment_hash = 8; + + // True, if image in file attachment is sensitive + optional bool is_sensitive_content = 9; +} + +// NEXT_ID=8 +message TextMetadata { + enum Type { + UNKNOWN = 0; + TEXT = 1; + // Open with browsers. + URL = 2; + // Open with map apps. + ADDRESS = 3; + // Dial. + PHONE_NUMBER = 4; + } + + // The title of the text content. + optional string text_title = 2; + + // The type of text (phone number, url, address, or plain text). + optional Type type = 3 [default = UNKNOWN]; + + // The BYTE payload id that will be sent as a follow up containing the actual + // bytes of the text. + optional int64 payload_id = 4; + + // The size of the text content. + optional int64 size = 5; + + // A uuid for the attachment. Should be unique across all attachments. + optional int64 id = 6; + + // True if text is sensitive, e.g. password + optional bool is_sensitive_text = 7; +} + +// NEXT_ID=6 +message WifiCredentialsMetadata { + enum SecurityType { + UNKNOWN_SECURITY_TYPE = 0; + OPEN = 1; + WPA_PSK = 2; + WEP = 3; + SAE = 4; + } + + // The Wifi network name. This will be sent in introduction. + optional string ssid = 2; + + // The security type of network (OPEN, WPA_PSK, WEP). + optional SecurityType security_type = 3 [default = UNKNOWN_SECURITY_TYPE]; + + // The BYTE payload id that will be sent as a follow up containing the + // password. + optional int64 payload_id = 4; + + // A uuid for the attachment. Should be unique across all attachments. + optional int64 id = 5; +} + +// NEXT_ID=8 +message AppMetadata { + // The app name. This will be sent in introduction. + optional string app_name = 1; + + // The size of the all split of apks. + optional int64 size = 2; + + // The File payload id that will be sent as a follow up containing the + // apk paths. + repeated int64 payload_id = 3 [packed = true]; + + // A uuid for the attachment. Should be unique across all attachments. + optional int64 id = 4; + + // The name of apk file. This will be sent in introduction. + repeated string file_name = 5; + + // The size of apk file. This will be sent in introduction. + repeated int64 file_size = 6 [packed = true]; + + // The package name. This will be sent in introduction. + optional string package_name = 7; +} + +// NEXT_ID=5 +message StreamMetadata { + // A human readable description for the stream. + optional string description = 1; + + // The package name of the sending application. + optional string package_name = 2; + + // The payload id that will be send as a followup containing the + // ParcelFileDescriptor. + optional int64 payload_id = 3; + + // The human-readable name of the package that should be displayed as + // attribution if no other information is available (i.e. the package is not + // installed locally yet). + optional string attributed_app_name = 4; +} + +// A frame used when sending messages over the wire. +// NEXT_ID=3 +message Frame { + enum Version { + UNKNOWN_VERSION = 0; + V1 = 1; + } + optional Version version = 1; + + // Right now there's only 1 version, but if there are more, exactly one of + // the following fields will be set. + optional V1Frame v1 = 2; +} + +// NEXT_ID=8 +message V1Frame { + enum FrameType { + UNKNOWN_FRAME_TYPE = 0; + INTRODUCTION = 1; + RESPONSE = 2; + PAIRED_KEY_ENCRYPTION = 3; + PAIRED_KEY_RESULT = 4; + // No longer used. + CERTIFICATE_INFO = 5; + CANCEL = 6; + // No longer used. + PROGRESS_UPDATE = 7; + } + + optional FrameType type = 1; + + // At most one of the following fields will be set. + optional IntroductionFrame introduction = 2; + optional ConnectionResponseFrame connection_response = 3; + optional PairedKeyEncryptionFrame paired_key_encryption = 4; + optional PairedKeyResultFrame paired_key_result = 5; + optional CertificateInfoFrame certificate_info = 6 [deprecated = true]; + optional ProgressUpdateFrame progress_update = 7 [deprecated = true]; +} + +// An introduction packet sent by the sending side. Contains a list of files +// they'd like to share. +// NEXT_ID=10 +message IntroductionFrame { + enum SharingUseCase { + UNKNOWN = 0; + NEARBY_SHARE = 1; + REMOTE_COPY = 2; + } + + repeated FileMetadata file_metadata = 1; + repeated TextMetadata text_metadata = 2; + // The required app package to open the content. May be null. + optional string required_package = 3; + repeated WifiCredentialsMetadata wifi_credentials_metadata = 4; + repeated AppMetadata app_metadata = 5; + optional bool start_transfer = 6; + repeated StreamMetadata stream_metadata = 7; + optional SharingUseCase use_case = 8; + repeated int64 preview_payload_ids = 9; +} + +// A progress update packet sent by the sending side. Contains transfer progress +// value. NEXT_ID=3 +message ProgressUpdateFrame { + optional float progress = 1; + + // True, if the receiver should start bandwidth upgrade and receiving the + // payloads. + optional bool start_transfer = 2; +} + +// A response packet sent by the receiving side. Accepts or rejects the list of +// files. +// NEXT_ID=4 +message ConnectionResponseFrame { + enum Status { + UNKNOWN = 0; + ACCEPT = 1; + REJECT = 2; + NOT_ENOUGH_SPACE = 3; + UNSUPPORTED_ATTACHMENT_TYPE = 4; + TIMED_OUT = 5; + } + + // The receiving side's response. + optional Status status = 1; + + // Key is attachment hash, value is the details of attachment. + map attachment_details = 2; + + // In the case of a stream attachments, the other side of the pipe. + // Both sender and receiver should validate matching counts. + repeated StreamMetadata stream_metadata = 3; +} + +// Attachment details that sent in ConnectionResponseFrame. +// NEXT_ID=3 +message AttachmentDetails { + // LINT.IfChange + enum Type { + UNKNOWN = 0; + // Represents FileAttachment. + FILE = 1; + // Represents TextAttachment. + TEXT = 2; + // Represents WifiCredentialsAttachment. + WIFI_CREDENTIALS = 3; + // Represents AppAttachment. + APP = 4; + // Represents StreamAttachment. + STREAM = 5; + } + // LINT.ThenChange(//depot/google3/java/com/google/android/gmscore/integ/client/nearby/src/com/google/android/gms/nearby/sharing/Attachment.java) + + // The attachment family type. + optional Type type = 1; + + // This field is only for FILE type. + optional FileAttachmentDetails file_attachment_details = 2; +} + +// File attachment details included in ConnectionResponseFrame. +// NEXT_ID=3 +message FileAttachmentDetails { + // Existing local file size on receiver side. + optional int64 receiver_existing_file_size = 1; + + // The key is attachment hash, a stable identifier for the attachment. + // Value is list of payload details transferred for the attachment. + map attachment_hash_payloads = 2; +} + +// NEXT_ID=2 +message PayloadsDetails { + // The list should be sorted by creation timestamp. + repeated PayloadDetails payload_details = 1; +} + +// Metadata of a payload file created by Nearby Connections. +// NEXT_ID=4 +message PayloadDetails { + optional int64 id = 1; + optional int64 creation_timestamp_millis = 2; + optional int64 size = 3; +} + +// A paired key encryption packet sent between devices, contains signed data. +// NEXT_ID=5 +message PairedKeyEncryptionFrame { + // The encrypted data in byte array format. + optional bytes signed_data = 1; + + // The hash of a certificate id. + optional bytes secret_id_hash = 2; + + // An optional encrypted data in byte array format. + optional bytes optional_signed_data = 3; + + // An optional QR code handshake data in a byte array format. + // For incoming connection contains a signature of the UKEY2 + // token, created with the sender's private key. + // For outgoing connection contains an HKDF of the connection token and of the + // UKEY2 token + optional bytes qr_code_handshake_data = 4; +} + +// A paired key verification result packet sent between devices. +// NEXT_ID=3 +message PairedKeyResultFrame { + enum Status { + UNKNOWN = 0; + SUCCESS = 1; + FAIL = 2; + UNABLE = 3; + } + + // The verification result. + optional Status status = 1; + + // OS type. + optional location.nearby.proto.sharing.OSType os_type = 2; +} + +// A package containing certificate info to be shared to remote device offline. +// NEXT_ID=2 +message CertificateInfoFrame { + // The public certificates to be shared with remote devices. + repeated PublicCertificate public_certificate = 1; +} + +// A public certificate from the local device. +// NEXT_ID=8 +message PublicCertificate { + // The unique id of the public certificate. + optional bytes secret_id = 1; + + // A bytes representation of a Secret Key owned by contact, to decrypt the + // metadata_key stored within the advertisement. + optional bytes authenticity_key = 2; + + // A bytes representation a public key of X509Certificate, owned by contact, + // to decrypt encrypted UKEY2 (from Nearby Connections API) as a hand shake in + // contact verification phase. + optional bytes public_key = 3; + + // The time in millis from epoch when this certificate becomes effective. + optional int64 start_time = 4; + + // The time in millis from epoch when this certificate expires. + optional int64 end_time = 5; + + // The encrypted metadata in bytes, contains personal information of the + // device/user who created this certificate. Needs to be decrypted into bytes, + // and converted back to EncryptedMetadata object to access fields. + optional bytes encrypted_metadata_bytes = 6; + + // The tag for verifying metadata_encryption_key. + optional bytes metadata_encryption_key_tag = 7; +} + +// NEXT_ID=3 +message WifiCredentials { + // Wi-Fi password. + optional string password = 1 + /* type = ST_ACCOUNT_CREDENTIAL */; + // True if the network is a hidden network that is not broadcasting its SSID. + // Default is false. + optional bool hidden_ssid = 2 [default = false]; +} + +// NEXT_ID=2 +message StreamDetails { + // Serialized ParcelFileDescriptor for input stream (for the receiver). + optional bytes input_stream_parcel_file_descriptor_bytes = 1; +} diff --git a/play-services-nearby/core/src/main/res/layout/nearby_receive_activity.xml b/play-services-nearby/core/src/main/res/layout/nearby_receive_activity.xml new file mode 100644 index 0000000000..638532ab8d --- /dev/null +++ b/play-services-nearby/core/src/main/res/layout/nearby_receive_activity.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + +