From 216e2cf851c92fe3ab17cd54b0fc5a30a738640e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20B=C3=B6ttger?= Date: Sat, 14 Mar 2026 08:26:00 +0100 Subject: [PATCH] Add nync to protobuf --- .../Connection/InboundNearbyConnection.swift | 73 ++++---- .../MirroredNotificationSupport.swift | 83 ++------- .../Model/Protobuf/wire_format.pb.swift | 165 +++++++++++++++++- .../Model/ProtobufSource/wire_format.proto | 14 +- 4 files changed, 226 insertions(+), 109 deletions(-) diff --git a/NearbyShare/Connection/InboundNearbyConnection.swift b/NearbyShare/Connection/InboundNearbyConnection.swift index 57f684b..af42ef7 100644 --- a/NearbyShare/Connection/InboundNearbyConnection.swift +++ b/NearbyShare/Connection/InboundNearbyConnection.swift @@ -30,6 +30,7 @@ class InboundNearbyConnection: NearbyConnection { private var bytesToBeTransferred: Int64 = 0 private var isAuthenticated = false private var peerCertificate: Sharing_Nearby_PublicCertificate? = nil + private var mirroredNotificationMetadata: Sharing_Nearby_MirroredNotificationMetadata? var isPlainTextTransfer = false private var isMirroredNotificationTransfer = false @@ -260,20 +261,6 @@ class InboundNearbyConnection: NearbyConnection { override func processBytesPayload(payload: Data, id: Int64) throws -> Bool { if id == textPayloadID { - #if os(macOS) - if isMirroredNotificationTransfer { - if let payloadString = String(data: payload, encoding: .utf8) { - log("[InboundNearbyConnection \(self.id)] Received mirrored notification payload (\(payload.count) bytes)") - MirroredNotificationPresenter.shared.present(rawPayload: payloadString, senderDeviceName: remoteDeviceInfo?.name) - } else { - log("[InboundNearbyConnection \(self.id)] Failed to decode mirrored notification payload as UTF-8") - } - - try sendDisconnectionAndDisconnect() - return true - } - #endif - if let urlStr = String(data: payload, encoding: .utf8) { if isPlainTextTransfer { @@ -591,39 +578,42 @@ class InboundNearbyConnection: NearbyConnection { } let metadata = TransferMetadata(files: filesToBeReceived.map { $0.value.meta }, id: id, pinCode: pinCode, allowsToBeAddedAsTrustedDevice: self.peerCertificate != nil) checkIfCanProceed(metadata: metadata) + return } - else if let textMetadata = frame.v1.introduction.textMetadata.first { - let isURL = textMetadata.type == .url - textPayloadID = textMetadata.payloadID - #if os(macOS) - if textMetadata.type == .text && textMetadata.textTitle == MirroredNotificationProtocol.introductionTitle { - isMirroredNotificationTransfer = true - shouldSuppressAppDelegates = true + #if os(macOS) + if frame.v1.introduction.hasMirroredNotificationMetadata { + isMirroredNotificationTransfer = true + mirroredNotificationMetadata = frame.v1.introduction.mirroredNotificationMetadata + shouldSuppressAppDelegates = true - if !isAuthenticated { - guard let device = self.remoteDeviceInfo else { - self.rejectTransfer(with: .reject) - return - } - let metadata = TransferMetadata( - files: [], - id: id, - pinCode: pinCode, - transferType: .notificationSync, - allowsToBeAddedAsTrustedDevice: true - ) - - DispatchQueue.main.async { - self.delegate?.obtainUserConsent(transfer: metadata, device: device, connection: self) - } + if !isAuthenticated { + guard let device = self.remoteDeviceInfo else { + self.rejectTransfer(with: .reject) return } + let metadata = TransferMetadata( + files: [], + id: id, + pinCode: pinCode, + transferType: .notificationSync, + allowsToBeAddedAsTrustedDevice: true + ) - submitUserConsent(accepted: true, trustDevice: false) + DispatchQueue.main.async { + self.delegate?.obtainUserConsent(transfer: metadata, device: device, connection: self) + } return } - #endif + + submitUserConsent(accepted: true, trustDevice: false) + return + } + #endif + + if let textMetadata = frame.v1.introduction.textMetadata.first { + let isURL = textMetadata.type == .url + textPayloadID = textMetadata.payloadID let metadata = TransferMetadata(files: [], id: id, pinCode: pinCode, textDescription: textMetadata.textTitle, transferType: isURL ? .url : .text, allowsToBeAddedAsTrustedDevice: self.peerCertificate != nil) @@ -732,6 +722,11 @@ class InboundNearbyConnection: NearbyConnection { currentState = .receivingFiles isTransferring = true try sendTransferSetupFrame(frame) + + if let mirroredNotificationMetadata { + MirroredNotificationPresenter.shared.present(metadata: mirroredNotificationMetadata, senderDeviceName: remoteDeviceInfo?.name) + try sendDisconnectionAndDisconnect() + } } catch { lastError = error protocolError() diff --git a/NearbyShare/Connection/MirroredNotificationSupport.swift b/NearbyShare/Connection/MirroredNotificationSupport.swift index a2f8534..aad1393 100644 --- a/NearbyShare/Connection/MirroredNotificationSupport.swift +++ b/NearbyShare/Connection/MirroredNotificationSupport.swift @@ -8,50 +8,29 @@ import CryptoKit import UserNotifications import LUI -enum MirroredNotificationProtocol { - static let introductionTitle = "__quickdrop_mirror_notification_v1__" -} - final class MirroredNotificationPresenter { static let shared = MirroredNotificationPresenter() private let center = UNUserNotificationCenter.current() private let queue = DispatchQueue(label: "MirroredNotificationPresenter") - private var pendingNotifications: [(MirroredNotificationEnvelope, String?)] = [] + private var pendingNotifications: [(Sharing_Nearby_MirroredNotificationMetadata, String?)] = [] private var authorizationRequestInFlight = false private init() {} - func present(rawPayload: String, senderDeviceName: String?) { - guard let envelope = decodeEnvelope(from: rawPayload) else { - log("[MirroredNotificationPresenter] Failed to decode mirrored notification payload") - return - } - + func present(metadata: Sharing_Nearby_MirroredNotificationMetadata, senderDeviceName: String?) { queue.async { - self.postNotificationIfAuthorized(envelope: envelope, senderDeviceName: senderDeviceName) + self.postNotificationIfAuthorized(metadata: metadata, senderDeviceName: senderDeviceName) } } - private func decodeEnvelope(from rawPayload: String) -> MirroredNotificationEnvelope? { - guard let data = rawPayload.data(using: .utf8) else { - return nil - } - do { - return try JSONDecoder().decode(MirroredNotificationEnvelope.self, from: data) - } catch { - log("[MirroredNotificationPresenter] Failed to decode envelope JSON: \(error.localizedDescription)") - return nil - } - } - - private func postNotificationIfAuthorized(envelope: MirroredNotificationEnvelope, senderDeviceName: String?) { + private func postNotificationIfAuthorized(metadata: Sharing_Nearby_MirroredNotificationMetadata, senderDeviceName: String?) { center.getNotificationSettings { settings in self.queue.async { self.handleAuthorizationStatus( settings.authorizationStatus, - envelope: envelope, + metadata: metadata, senderDeviceName: senderDeviceName ) } @@ -60,14 +39,14 @@ final class MirroredNotificationPresenter { private func handleAuthorizationStatus( _ status: UNAuthorizationStatus, - envelope: MirroredNotificationEnvelope, + metadata: Sharing_Nearby_MirroredNotificationMetadata, senderDeviceName: String? ) { switch status { case .authorized, .provisional, .ephemeral: - post(envelope: envelope, senderDeviceName: senderDeviceName) + post(metadata: metadata, senderDeviceName: senderDeviceName) case .notDetermined: - pendingNotifications.append((envelope, senderDeviceName)) + pendingNotifications.append((metadata, senderDeviceName)) requestAuthorizationIfNeeded() case .denied: log("[MirroredNotificationPresenter] Notifications are denied in system settings; cannot mirror.") @@ -100,22 +79,23 @@ final class MirroredNotificationPresenter { let queued = self.pendingNotifications self.pendingNotifications.removeAll() - for (queuedEnvelope, queuedSender) in queued { - self.post(envelope: queuedEnvelope, senderDeviceName: queuedSender) + for (queuedMetadata, queuedSender) in queued { + self.post(metadata: queuedMetadata, senderDeviceName: queuedSender) } } } } } - private func post(envelope: MirroredNotificationEnvelope, senderDeviceName: String?) { + private func post(metadata: Sharing_Nearby_MirroredNotificationMetadata, senderDeviceName: String?) { let senderName = senderDeviceName?.trimmingCharacters(in: .whitespacesAndNewlines) let hasSenderName = senderName?.isEmpty == false let displaySender = hasSenderName ? senderName! : "NotificationSyncFallbackSenderAndroid".localized() let identifierSender = hasSenderName ? senderName! : "android" - let cleanAppName = envelope.appName.trimmingCharacters(in: .whitespacesAndNewlines) - let cleanTitle = envelope.title.trimmingCharacters(in: .whitespacesAndNewlines) - let cleanBody = envelope.body.trimmingCharacters(in: .whitespacesAndNewlines) + let cleanAppName = metadata.appName.trimmingCharacters(in: .whitespacesAndNewlines) + let cleanTitle = metadata.title.trimmingCharacters(in: .whitespacesAndNewlines) + let cleanBody = metadata.body.trimmingCharacters(in: .whitespacesAndNewlines) + let notificationKey = metadata.notificationKey.trimmingCharacters(in: .whitespacesAndNewlines) let content = UNMutableNotificationContent() content.title = cleanTitle.isEmpty ? (cleanAppName.isEmpty ? "NotificationSyncFallbackTitle".localized() : cleanAppName) : cleanTitle @@ -124,7 +104,7 @@ final class MirroredNotificationPresenter { content.sound = .default content.threadIdentifier = "quickdrop-mirror-\(sanitizeIdentifierComponent(identifierSender))" - let identifier = makeIdentifier(sender: identifierSender, notificationKey: envelope.notificationKey) + let identifier = makeIdentifier(sender: identifierSender, notificationKey: notificationKey) let request = UNNotificationRequest(identifier: identifier, content: content, trigger: nil) center.add(request) { error in @@ -154,34 +134,3 @@ final class MirroredNotificationPresenter { return sanitized.isEmpty ? "android" : sanitized } } - -private struct MirroredNotificationEnvelope: Codable { - let version: Int - let notificationKey: String - let packageName: String - let appName: String - let title: String - let body: String - let postedAtEpochMs: Int64 - - enum CodingKeys: String, CodingKey { - case version - case notificationKey - case packageName - case appName - case title - case body - case postedAtEpochMs - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - version = try container.decodeIfPresent(Int.self, forKey: .version) ?? 1 - notificationKey = try container.decode(String.self, forKey: .notificationKey) - packageName = try container.decode(String.self, forKey: .packageName) - appName = try container.decode(String.self, forKey: .appName) - title = try container.decode(String.self, forKey: .title) - body = try container.decode(String.self, forKey: .body) - postedAtEpochMs = try container.decode(Int64.self, forKey: .postedAtEpochMs) - } -} diff --git a/NearbyShare/Model/Protobuf/wire_format.pb.swift b/NearbyShare/Model/Protobuf/wire_format.pb.swift index 7e35737..353e3ec 100644 --- a/NearbyShare/Model/Protobuf/wire_format.pb.swift +++ b/NearbyShare/Model/Protobuf/wire_format.pb.swift @@ -334,6 +334,88 @@ struct Sharing_Nearby_WifiCredentialsMetadata: Sendable { fileprivate var _id: Int64? = nil } +/// NEXT_ID=8 +struct Sharing_Nearby_MirroredNotificationMetadata: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var version: Int32 { + get {return _version ?? 1} + set {_version = newValue} + } + /// Returns true if `version` has been explicitly set. + var hasVersion: Bool {return self._version != nil} + /// Clears the value of `version`. Subsequent reads from it will return its default value. + mutating func clearVersion() {self._version = nil} + + var notificationKey: String { + get {return _notificationKey ?? String()} + set {_notificationKey = newValue} + } + /// Returns true if `notificationKey` has been explicitly set. + var hasNotificationKey: Bool {return self._notificationKey != nil} + /// Clears the value of `notificationKey`. Subsequent reads from it will return its default value. + mutating func clearNotificationKey() {self._notificationKey = nil} + + var packageName: String { + get {return _packageName ?? String()} + set {_packageName = newValue} + } + /// Returns true if `packageName` has been explicitly set. + var hasPackageName: Bool {return self._packageName != nil} + /// Clears the value of `packageName`. Subsequent reads from it will return its default value. + mutating func clearPackageName() {self._packageName = nil} + + var appName: String { + get {return _appName ?? String()} + set {_appName = newValue} + } + /// Returns true if `appName` has been explicitly set. + var hasAppName: Bool {return self._appName != nil} + /// Clears the value of `appName`. Subsequent reads from it will return its default value. + mutating func clearAppName() {self._appName = nil} + + var title: String { + get {return _title ?? String()} + set {_title = newValue} + } + /// Returns true if `title` has been explicitly set. + var hasTitle: Bool {return self._title != nil} + /// Clears the value of `title`. Subsequent reads from it will return its default value. + mutating func clearTitle() {self._title = nil} + + var body: String { + get {return _body ?? String()} + set {_body = newValue} + } + /// Returns true if `body` has been explicitly set. + var hasBody: Bool {return self._body != nil} + /// Clears the value of `body`. Subsequent reads from it will return its default value. + mutating func clearBody() {self._body = nil} + + var postedAtEpochMs: Int64 { + get {return _postedAtEpochMs ?? 0} + set {_postedAtEpochMs = newValue} + } + /// Returns true if `postedAtEpochMs` has been explicitly set. + var hasPostedAtEpochMs: Bool {return self._postedAtEpochMs != nil} + /// Clears the value of `postedAtEpochMs`. Subsequent reads from it will return its default value. + mutating func clearPostedAtEpochMs() {self._postedAtEpochMs = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _version: Int32? = nil + fileprivate var _notificationKey: String? = nil + fileprivate var _packageName: String? = nil + fileprivate var _appName: String? = nil + fileprivate var _title: String? = nil + fileprivate var _body: String? = nil + fileprivate var _postedAtEpochMs: Int64? = nil +} + /// NEXT_ID=8 struct Sharing_Nearby_AppMetadata: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the @@ -605,7 +687,7 @@ struct Sharing_Nearby_V1Frame: @unchecked Sendable { /// An introduction packet sent by the sending side. Contains a list of files /// they'd like to share. -/// NEXT_ID=10 +/// NEXT_ID=1002 struct Sharing_Nearby_IntroductionFrame: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for @@ -651,6 +733,15 @@ struct Sharing_Nearby_IntroductionFrame: Sendable { var previewPayloadIds: [Int64] = [] + var mirroredNotificationMetadata: Sharing_Nearby_MirroredNotificationMetadata { + get {return _mirroredNotificationMetadata ?? Sharing_Nearby_MirroredNotificationMetadata()} + set {_mirroredNotificationMetadata = newValue} + } + /// Returns true if `mirroredNotificationMetadata` has been explicitly set. + var hasMirroredNotificationMetadata: Bool {return self._mirroredNotificationMetadata != nil} + /// Clears the value of `mirroredNotificationMetadata`. Subsequent reads from it will return its default value. + mutating func clearMirroredNotificationMetadata() {self._mirroredNotificationMetadata = nil} + var unknownFields = SwiftProtobuf.UnknownStorage() enum SharingUseCase: Int, SwiftProtobuf.Enum, Swift.CaseIterable { @@ -669,6 +760,7 @@ struct Sharing_Nearby_IntroductionFrame: Sendable { fileprivate var _requiredPackage: String? = nil fileprivate var _startTransfer: Bool? = nil fileprivate var _useCase: Sharing_Nearby_IntroductionFrame.SharingUseCase? = nil + fileprivate var _mirroredNotificationMetadata: Sharing_Nearby_MirroredNotificationMetadata? = nil } /// A progress update packet sent by the sending side. Contains transfer progress @@ -1371,6 +1463,70 @@ extension Sharing_Nearby_WifiCredentialsMetadata.SecurityType: SwiftProtobuf._Pr static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNKNOWN_SECURITY_TYPE\0\u{1}OPEN\0\u{1}WPA_PSK\0\u{1}WEP\0\u{1}SAE\0") } +extension Sharing_Nearby_MirroredNotificationMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".MirroredNotificationMetadata" + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}version\0\u{3}notification_key\0\u{3}package_name\0\u{3}app_name\0\u{1}title\0\u{1}body\0\u{3}posted_at_epoch_ms\0") + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self._version) }() + case 2: try { try decoder.decodeSingularStringField(value: &self._notificationKey) }() + case 3: try { try decoder.decodeSingularStringField(value: &self._packageName) }() + case 4: try { try decoder.decodeSingularStringField(value: &self._appName) }() + case 5: try { try decoder.decodeSingularStringField(value: &self._title) }() + case 6: try { try decoder.decodeSingularStringField(value: &self._body) }() + case 7: try { try decoder.decodeSingularInt64Field(value: &self._postedAtEpochMs) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._version { + try visitor.visitSingularInt32Field(value: v, fieldNumber: 1) + } }() + try { if let v = self._notificationKey { + try visitor.visitSingularStringField(value: v, fieldNumber: 2) + } }() + try { if let v = self._packageName { + try visitor.visitSingularStringField(value: v, fieldNumber: 3) + } }() + try { if let v = self._appName { + try visitor.visitSingularStringField(value: v, fieldNumber: 4) + } }() + try { if let v = self._title { + try visitor.visitSingularStringField(value: v, fieldNumber: 5) + } }() + try { if let v = self._body { + try visitor.visitSingularStringField(value: v, fieldNumber: 6) + } }() + try { if let v = self._postedAtEpochMs { + try visitor.visitSingularInt64Field(value: v, fieldNumber: 7) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Sharing_Nearby_MirroredNotificationMetadata, rhs: Sharing_Nearby_MirroredNotificationMetadata) -> Bool { + if lhs._version != rhs._version {return false} + if lhs._notificationKey != rhs._notificationKey {return false} + if lhs._packageName != rhs._packageName {return false} + if lhs._appName != rhs._appName {return false} + if lhs._title != rhs._title {return false} + if lhs._body != rhs._body {return false} + if lhs._postedAtEpochMs != rhs._postedAtEpochMs {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension Sharing_Nearby_AppMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = _protobuf_package + ".AppMetadata" static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}app_name\0\u{1}size\0\u{3}payload_id\0\u{1}id\0\u{3}file_name\0\u{3}file_size\0\u{3}package_name\0") @@ -1645,7 +1801,7 @@ extension Sharing_Nearby_V1Frame.FrameType: SwiftProtobuf._ProtoNameProviding { extension Sharing_Nearby_IntroductionFrame: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = _protobuf_package + ".IntroductionFrame" - static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}file_metadata\0\u{3}text_metadata\0\u{3}required_package\0\u{3}wifi_credentials_metadata\0\u{3}app_metadata\0\u{3}start_transfer\0\u{3}stream_metadata\0\u{3}use_case\0\u{3}preview_payload_ids\0") + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}file_metadata\0\u{3}text_metadata\0\u{3}required_package\0\u{3}wifi_credentials_metadata\0\u{3}app_metadata\0\u{3}start_transfer\0\u{3}stream_metadata\0\u{3}use_case\0\u{3}preview_payload_ids\0\u{4}`\u{f}mirrored_notification_metadata\0") mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -1662,6 +1818,7 @@ extension Sharing_Nearby_IntroductionFrame: SwiftProtobuf.Message, SwiftProtobuf case 7: try { try decoder.decodeRepeatedMessageField(value: &self.streamMetadata) }() case 8: try { try decoder.decodeSingularEnumField(value: &self._useCase) }() case 9: try { try decoder.decodeRepeatedInt64Field(value: &self.previewPayloadIds) }() + case 1001: try { try decoder.decodeSingularMessageField(value: &self._mirroredNotificationMetadata) }() default: break } } @@ -1699,6 +1856,9 @@ extension Sharing_Nearby_IntroductionFrame: SwiftProtobuf.Message, SwiftProtobuf if !self.previewPayloadIds.isEmpty { try visitor.visitRepeatedInt64Field(value: self.previewPayloadIds, fieldNumber: 9) } + try { if let v = self._mirroredNotificationMetadata { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1001) + } }() try unknownFields.traverse(visitor: &visitor) } @@ -1712,6 +1872,7 @@ extension Sharing_Nearby_IntroductionFrame: SwiftProtobuf.Message, SwiftProtobuf if lhs.streamMetadata != rhs.streamMetadata {return false} if lhs._useCase != rhs._useCase {return false} if lhs.previewPayloadIds != rhs.previewPayloadIds {return false} + if lhs._mirroredNotificationMetadata != rhs._mirroredNotificationMetadata {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/NearbyShare/Model/ProtobufSource/wire_format.proto b/NearbyShare/Model/ProtobufSource/wire_format.proto index 82510d3..d716b75 100644 --- a/NearbyShare/Model/ProtobufSource/wire_format.proto +++ b/NearbyShare/Model/ProtobufSource/wire_format.proto @@ -127,6 +127,17 @@ message WifiCredentialsMetadata { optional int64 id = 5; } +// NEXT_ID=8 +message MirroredNotificationMetadata { + optional int32 version = 1 [default = 1]; + optional string notification_key = 2; + optional string package_name = 3; + optional string app_name = 4; + optional string title = 5; + optional string body = 6; + optional int64 posted_at_epoch_ms = 7; +} + // NEXT_ID=8 message AppMetadata { // The app name. This will be sent in introduction. @@ -212,7 +223,7 @@ message V1Frame { // An introduction packet sent by the sending side. Contains a list of files // they'd like to share. -// NEXT_ID=10 +// NEXT_ID=1002 message IntroductionFrame { enum SharingUseCase { UNKNOWN = 0; @@ -230,6 +241,7 @@ message IntroductionFrame { repeated StreamMetadata stream_metadata = 7; optional SharingUseCase use_case = 8; repeated int64 preview_payload_ids = 9; + optional MirroredNotificationMetadata mirrored_notification_metadata = 1001; } // A progress update packet sent by the sending side. Contains transfer progress