// 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. #import "third_party/nearby/cpp/platform/impl/ios/Source/GNCDiscoverer.h" #include #include #include "third_party/absl/functional/bind_front.h" #include "third_party/nearby/cpp/core/core.h" #include "third_party/nearby/cpp/core/listeners.h" #include "third_party/nearby/cpp/core/options.h" #include "third_party/nearby/cpp/core/status.h" #include "third_party/nearby/cpp/platform/base/byte_array.h" #import "third_party/nearby/cpp/platform/impl/ios/Source/GNCConnection.h" #import "third_party/nearby/cpp/platform/impl/ios/Source/Internal/GNCCore.h" #import "third_party/nearby/cpp/platform/impl/ios/Source/Internal/GNCCoreConnection.h" #import "third_party/nearby/cpp/platform/impl/ios/Source/Internal/GNCPayloadListener.h" #import "third_party/nearby/cpp/platform/impl/ios/Source/Internal/GNCUtils.h" #import "third_party/nearby/cpp/platform/impl/ios/Source/Platform/utils.h" #import "third_party/objective_c/google_toolbox_for_mac/Foundation/GTMLogger.h" NS_ASSUME_NONNULL_BEGIN using ::location::nearby::ByteArray; using ::location::nearby::CppStringFromObjCString; using ::location::nearby::connections::ConnectionOptions; using ::location::nearby::connections::DiscoveryListener; using ::location::nearby::connections::DistanceInfo; using ::location::nearby::connections::GNCStrategyToStrategy; using ResultListener = ::location::nearby::connections::ResultCallback; using ::location::nearby::connections::Status; /** This is a GNCDiscovererConnectionInfo that provides storage for its properties. */ @interface GNCDiscovererConnectionInfo : NSObject @property(nonatomic, copy) NSString *authToken; /** Creates a GNCDiscovererConnectionInfo object. */ + (instancetype)infoWithAuthToken:(NSString *)authToken; @end @implementation GNCDiscovererConnectionInfo + (instancetype)infoWithAuthToken:(NSString *)authToken { GNCDiscovererConnectionInfo *info = [[GNCDiscovererConnectionInfo alloc] init]; info.authToken = authToken; return info; } @end /** This is a GNCDiscoveredEndpointInfo that provides storage for its properties. */ @interface GNCDiscoveredEndpointInfo : NSObject @property(nonatomic, copy) NSString *name; @end @implementation GNCDiscoveredEndpointInfo @synthesize requestConnection = _requestConnection; + (instancetype)infoWithName:(NSString *)name requestConnection:(GNCConnectionRequester)requestConnection { GNCDiscoveredEndpointInfo *info = [[GNCDiscoveredEndpointInfo alloc] init]; info.name = name; info->_requestConnection = requestConnection; return info; } @end /** Information retained by the discoverer about each discovered endpoint. */ @interface GNCDiscovererEndpointInfo : NSObject /** Handles lostHandler once |onEndpointLost| has been callback. */ @property(nonatomic) GNCEndpointLostHandler lostHandler; /** The connInitHandler is stored after requestConnection. */ @property(nonatomic, nullable) GNCDiscovererConnectionInitializationHandler connInitHandler; /** The connFailureHandler is stored after requestConnection. */ @property(nonatomic, nullable) GNCConnectionFailureHandler connFailureHandler; /** Client responses Accept or Reject. */ @property(nonatomic) GNCConnectionResponse clientResponse; /** Whether the client response has been received. */ @property(nonatomic) BOOL clientResponseReceived; /** * The connectionhandler returned by connInitHandler. Stored here if the connection is accepted. */ @property(nonatomic, nullable) GNCConnectionHandler connectionHandler; /** @c GNCCoreConnection is created and stored if connection is accepted. */ @property(nonatomic, weak) GNCCoreConnection *connection; /** @c GNCConnectionHandlers object is returned by connectionHandler and stored here. */ @property(nonatomic) GNCConnectionHandlers *connectionHandlers; @end @implementation GNCDiscovererEndpointInfo @end /** GNCDiscoverer members. */ @interface GNCDiscoverer () @property(nonatomic) GNCCore *core; @property(nonatomic) GNCEndpointFoundHandler endpointFoundHandler; @property(nonatomic, assign) Status status; @property(nonatomic) NSMapTable *endpoints; @end /** C++ classes passed to the core library by GNCDiscoverer. */ namespace location { namespace nearby { namespace connections { /** This class contains the discoverer callbacks related to a connection. */ class GNCDiscovererConnectionListener { public: GNCDiscovererConnectionListener(GNCCore *core, NSMapTable *endpoints) : core_(core), endpoints_(endpoints) {} void OnInitiated(const std::string &endpoint_id, const ConnectionResponseInfo &info) { NSString *endpointId = ObjCStringFromCppString(endpoint_id); GNCDiscovererEndpointInfo *endpointInfo = [endpoints_ objectForKey:endpointId]; if (!endpointInfo) { return; } // Call the connection initiation handler. Synchronous because it returns the connection // handler. NSString *authToken = ObjCStringFromCppString(info.authentication_token); GNCCore *core = core_; // don't capture |this| dispatch_sync(dispatch_get_main_queue(), ^{ endpointInfo.connectionHandler = endpointInfo.connInitHandler( [GNCDiscovererConnectionInfo infoWithAuthToken:authToken], ^(GNCConnectionResponse response) { endpointInfo.clientResponse = response; endpointInfo.clientResponseReceived = YES; if (response == GNCConnectionResponseAccept) { // The connect was accepted by the client. if (payload_listener_ == nullptr) { payload_listener_ = std::make_unique( core, ^{ return endpointInfo.connectionHandlers; }, ^{ return endpointInfo.connection.payloads; }); } core->_core->AcceptConnection( CppStringFromObjCString(endpointId), PayloadListener{ .payload_cb = absl::bind_front(&GNCPayloadListener::OnPayload, payload_listener_.get()), .payload_progress_cb = absl::bind_front( &GNCPayloadListener::OnPayloadProgress, payload_listener_.get()), }, ResultListener{}); } else { // The connect was rejected by the client. core->_core->RejectConnection(CppStringFromObjCString(endpointId), ResultListener{}); } }); }); } void OnAccepted(const std::string &endpoint_id) { NSString *endpointId = ObjCStringFromCppString(endpoint_id); GNCDiscovererEndpointInfo *endpointInfo = [endpoints_ objectForKey:endpointId]; if (!endpointInfo) { return; } // The connection has been accepted by both endpoints, so create the GNCConnection object // and pass it to |successHandler| for the client to use. // Note: Use a local strong reference to the connection object; don't just assign to // |endpointInfo.connection|. Without a strong reference, the connection object can be // deallocated before |successHandler| is called in the Release build. id connection = [GNCCoreConnection connectionWithEndpointId:endpointId core:core_ deallocHandler:^{ // Don't remove the remote endpoint (like GNCAdvertiser does) because that's // done when the endpoint is lost. }]; endpointInfo.connection = connection; // Callback is synchronous because it returns the connection handlers. dispatch_sync(dispatch_get_main_queue(), ^{ endpointInfo.connectionHandlers = endpointInfo.connectionHandler(endpointInfo.connection); }); endpointInfo.clientResponseReceived = NO; // support reconnection after disconnection } void OnRejected(const std::string &endpoint_id, Status status) { NSString *endpointId = ObjCStringFromCppString(endpoint_id); GNCDiscovererEndpointInfo *endpointInfo = [endpoints_ objectForKey:endpointId]; if (!endpointInfo) { return; } // If either side rejected, call failureHandler with the connection status. dispatch_async(dispatch_get_main_queue(), ^{ endpointInfo.connFailureHandler(GNCConnectionFailureRejected); }); endpointInfo.clientResponseReceived = NO; // support reconnection after disconnection } void OnDisconnected(const std::string &endpoint_id) { NSString *endpointId = ObjCStringFromCppString(endpoint_id); GNCDiscovererEndpointInfo *endpointInfo = [endpoints_ objectForKey:endpointId]; if (!endpointInfo) { return; } if (endpointInfo.connection) { GNCDisconnectedHandler disconnectedHandler = endpointInfo.connectionHandlers.disconnectedHandler; dispatch_async(dispatch_get_main_queue(), ^{ if (disconnectedHandler) disconnectedHandler(GNCDisconnectedReasonUnknown); }); } } void OnBandwidthChanged(const std::string &endpoint_id, Medium medium) { // TODO(b/169292092): Implement. } private: GNCCore *core_; NSMapTable *endpoints_; std::unique_ptr payload_listener_; }; class GNCDiscoveryListener { public: explicit GNCDiscoveryListener(GNCDiscoverer *discoverer) : discoverer_(discoverer) {} void OnEndpointFound(const std::string &endpoint_id, const ByteArray &endpoint_info, const std::string &service_id) { GNCDiscoverer *discoverer = discoverer_; // strongify if (!discoverer) { return; } NSString *endpointId = ObjCStringFromCppString(endpoint_id); if ([discoverer.endpoints objectForKey:endpointId] != nil) { GTMLoggerError(@"Endpoint already discovered: %@", endpointId); } else { // The GNCDiscoveredEndpointInfo object created here lives as long as the client has strong // reference to it. Here's the chain of strong references maintained here: // client -> GNCDiscoveredEndpointInfo -> RequestConnection block -> // GNCDiscovererEndpointInfo (stored weakly in the |endpoints| map table) GNCDiscovererEndpointInfo *endpointInfo = [[GNCDiscovererEndpointInfo alloc] init]; [discoverer.endpoints setObject:endpointInfo forKey:endpointId]; // TODO(b/169292092): endpointInfo is an advertisement byte array. Need to implement to // extract the endpoint name not just force to cast string. NSString *name = ObjCStringFromCppString(std::string(endpoint_info)); GNCCore *core = discoverer.core; // don't capture |this| or |discoverer| NSMapTable *endpoints = discoverer.endpoints; GNCDiscoveredEndpointInfo *discEndpointInfo = [GNCDiscoveredEndpointInfo infoWithName:name requestConnection:^(NSString *name, GNCDiscovererConnectionInitializationHandler connInitHandler, GNCConnectionFailureHandler connFailureHandler) { endpointInfo.connInitHandler = connInitHandler; endpointInfo.connFailureHandler = connFailureHandler; if (discoverer_connection_listener_ == nullptr) { discoverer_connection_listener_ = std::make_unique(core, endpoints); } ConnectionListener listener = { .initiated_cb = absl::bind_front(&GNCDiscovererConnectionListener::OnInitiated, discoverer_connection_listener_.get()), .accepted_cb = absl::bind_front(&GNCDiscovererConnectionListener::OnAccepted, discoverer_connection_listener_.get()), .rejected_cb = absl::bind_front(&GNCDiscovererConnectionListener::OnRejected, discoverer_connection_listener_.get()), .disconnected_cb = absl::bind_front(&GNCDiscovererConnectionListener::OnDisconnected, discoverer_connection_listener_.get()), }; core->_core->RequestConnection( CppStringFromObjCString(endpointId), ConnectionRequestInfo{.endpoint_info = std::move(endpoint_info), .listener = std::move(listener)}, ConnectionOptions{}, ResultListener{.result_cb = [&connFailureHandler](Status status) { if (!status.Ok()) { dispatch_sync(dispatch_get_main_queue(), ^{ connFailureHandler(GNCConnectionFailureUnknown); }); } }}); }]; // Call the client endpoint-found handler. Tail call for reentrancy. dispatch_sync(dispatch_get_main_queue(), ^{ endpointInfo.lostHandler = discoverer.endpointFoundHandler(endpointId, discEndpointInfo); }); } } void OnEndpointLost(const std::string &endpoint_id) { GNCDiscoverer *discoverer = discoverer_; // strongify if (!discoverer) { return; } NSString *endpointId = ObjCStringFromCppString(endpoint_id); GNCDiscovererEndpointInfo *info = [discoverer.endpoints objectForKey:endpointId]; if (!info) { GTMLoggerError(@"Endpoint already lost: %@", endpointId); } else { dispatch_async(dispatch_get_main_queue(), ^{ info.lostHandler(); }); } [discoverer.endpoints removeObjectForKey:endpointId]; } void OnEndpointDistanceChanged_cb(const std::string &endpoint_id, DistanceInfo info) { // TODO(b/169292092): Implement. } private: __weak GNCDiscoverer *discoverer_; std::unique_ptr discoverer_connection_listener_; }; } // namespace connections } // namespace nearby } // namespace location using ::location::nearby::connections::GNCDiscoveryListener; @interface GNCDiscoverer () { std::unique_ptr discoveryListener; }; @end @implementation GNCDiscoverer + (instancetype)discovererWithServiceId:(NSString *)serviceId strategy:(GNCStrategy)strategy endpointFoundHandler:(GNCEndpointFoundHandler)endpointFoundHandler { GNCDiscoverer *discoverer = [[GNCDiscoverer alloc] init]; discoverer.endpointFoundHandler = endpointFoundHandler; discoverer.endpoints = [NSMapTable strongToWeakObjectsMapTable]; discoverer.core = GNCGetCore(); discoverer->discoveryListener = std::make_unique(discoverer); DiscoveryListener listener = { .endpoint_found_cb = absl::bind_front(&GNCDiscoveryListener::OnEndpointFound, discoverer->discoveryListener.get()), .endpoint_lost_cb = absl::bind_front(&GNCDiscoveryListener::OnEndpointLost, discoverer->discoveryListener.get()), }; discoverer.core->_core->StartDiscovery(CppStringFromObjCString(serviceId), ConnectionOptions{ .strategy = GNCStrategyToStrategy(strategy), }, std::move(listener), ResultListener{}); return discoverer; } - (void)dealloc { GTMLoggerInfo(@"GNCDiscoverer deallocated"); _core->_core->StopDiscovery(ResultListener{}); } @end NS_ASSUME_NONNULL_END