|  | // Copyright 2017 The Chromium Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | #import "FirebaseDatabasePlugin.h" | 
|  |  | 
|  | #import <Firebase/Firebase.h> | 
|  |  | 
|  | @interface FLTFirebaseDatabasePlugin () | 
|  | @end | 
|  |  | 
|  | @interface NSError (FlutterError) | 
|  | @property(readonly, nonatomic) FlutterError *flutterError; | 
|  | @property(readonly, nonatomic) NSDictionary *dictionary; | 
|  | @end | 
|  |  | 
|  | @implementation NSError (FlutterError) | 
|  | - (FlutterError *)flutterError { | 
|  | return [FlutterError errorWithCode:[NSString stringWithFormat:@"Error %ld", self.code] | 
|  | message:self.domain | 
|  | details:self.localizedDescription]; | 
|  | } | 
|  |  | 
|  | - (NSDictionary *)dictionary { | 
|  | return @{ | 
|  | @"code" : @(self.code), | 
|  | @"message" : self.domain ?: [NSNull null], | 
|  | @"details" : self.localizedDescription ?: [NSNull null], | 
|  | }; | 
|  | } | 
|  | @end | 
|  |  | 
|  | FIRDatabaseReference *getReference(FIRDatabase *database, NSDictionary *arguments) { | 
|  | NSString *path = arguments[@"path"]; | 
|  | FIRDatabaseReference *ref = database.reference; | 
|  | if ([path length] > 0) ref = [ref child:path]; | 
|  | return ref; | 
|  | } | 
|  |  | 
|  | FIRDatabaseQuery *getDatabaseQuery(FIRDatabase *database, NSDictionary *arguments) { | 
|  | FIRDatabaseQuery *query = getReference(database, arguments); | 
|  | NSDictionary *parameters = arguments[@"parameters"]; | 
|  | NSString *orderBy = parameters[@"orderBy"]; | 
|  | if ([orderBy isEqualToString:@"child"]) { | 
|  | query = [query queryOrderedByChild:parameters[@"orderByChildKey"]]; | 
|  | } else if ([orderBy isEqualToString:@"key"]) { | 
|  | query = [query queryOrderedByKey]; | 
|  | } else if ([orderBy isEqualToString:@"value"]) { | 
|  | query = [query queryOrderedByValue]; | 
|  | } else if ([orderBy isEqualToString:@"priority"]) { | 
|  | query = [query queryOrderedByPriority]; | 
|  | } | 
|  | id startAt = parameters[@"startAt"]; | 
|  | if (startAt) { | 
|  | id startAtKey = parameters[@"startAtKey"]; | 
|  | if (startAtKey) { | 
|  | query = [query queryStartingAtValue:startAt childKey:startAtKey]; | 
|  | } else { | 
|  | query = [query queryStartingAtValue:startAt]; | 
|  | } | 
|  | } | 
|  | id endAt = parameters[@"endAt"]; | 
|  | if (endAt) { | 
|  | id endAtKey = parameters[@"endAtKey"]; | 
|  | if (endAtKey) { | 
|  | query = [query queryEndingAtValue:endAt childKey:endAtKey]; | 
|  | } else { | 
|  | query = [query queryEndingAtValue:endAt]; | 
|  | } | 
|  | } | 
|  | id equalTo = parameters[@"equalTo"]; | 
|  | if (equalTo) { | 
|  | id equalToKey = parameters[@"equalToKey"]; | 
|  | if (equalToKey) { | 
|  | query = [query queryEqualToValue:equalTo childKey:equalToKey]; | 
|  | } else { | 
|  | query = [query queryEqualToValue:equalTo]; | 
|  | } | 
|  | } | 
|  | NSNumber *limitToFirst = parameters[@"limitToFirst"]; | 
|  | if (limitToFirst) { | 
|  | query = [query queryLimitedToFirst:limitToFirst.intValue]; | 
|  | } | 
|  | NSNumber *limitToLast = parameters[@"limitToLast"]; | 
|  | if (limitToLast) { | 
|  | query = [query queryLimitedToLast:limitToLast.intValue]; | 
|  | } | 
|  | return query; | 
|  | } | 
|  |  | 
|  | FIRDataEventType parseEventType(NSString *eventTypeString) { | 
|  | if ([@"_EventType.childAdded" isEqual:eventTypeString]) { | 
|  | return FIRDataEventTypeChildAdded; | 
|  | } else if ([@"_EventType.childRemoved" isEqual:eventTypeString]) { | 
|  | return FIRDataEventTypeChildRemoved; | 
|  | } else if ([@"_EventType.childChanged" isEqual:eventTypeString]) { | 
|  | return FIRDataEventTypeChildChanged; | 
|  | } else if ([@"_EventType.childMoved" isEqual:eventTypeString]) { | 
|  | return FIRDataEventTypeChildMoved; | 
|  | } else if ([@"_EventType.value" isEqual:eventTypeString]) { | 
|  | return FIRDataEventTypeValue; | 
|  | } | 
|  | assert(false); | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | id roundDoubles(id value) { | 
|  | // Workaround for https://github.com/firebase/firebase-ios-sdk/issues/91 | 
|  | // The Firebase iOS SDK sometimes returns doubles when ints were stored. | 
|  | // We detect doubles that can be converted to ints without loss of precision | 
|  | // and convert them. | 
|  | if ([value isKindOfClass:[NSNumber class]]) { | 
|  | CFNumberType type = CFNumberGetType((CFNumberRef)value); | 
|  | if (type == kCFNumberDoubleType || type == kCFNumberFloatType) { | 
|  | if ((double)(long long)[value doubleValue] == [value doubleValue]) { | 
|  | return [NSNumber numberWithLongLong:(long long)[value doubleValue]]; | 
|  | } | 
|  | } | 
|  | } else if ([value isKindOfClass:[NSArray class]]) { | 
|  | NSMutableArray *result = [NSMutableArray arrayWithCapacity:[value count]]; | 
|  | [value enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { | 
|  | [result addObject:roundDoubles(obj)]; | 
|  | }]; | 
|  | return result; | 
|  | } else if ([value isKindOfClass:[NSDictionary class]]) { | 
|  | NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:[value count]]; | 
|  | [value enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { | 
|  | result[key] = roundDoubles(obj); | 
|  | }]; | 
|  | return result; | 
|  | } | 
|  | return value; | 
|  | } | 
|  |  | 
|  | @interface FLTFirebaseDatabasePlugin () | 
|  | @property(nonatomic, retain) FlutterMethodChannel *channel; | 
|  | @end | 
|  |  | 
|  | @implementation FLTFirebaseDatabasePlugin | 
|  |  | 
|  | + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar { | 
|  | FlutterMethodChannel *channel = | 
|  | [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/firebase_database" | 
|  | binaryMessenger:[registrar messenger]]; | 
|  | FLTFirebaseDatabasePlugin *instance = [[FLTFirebaseDatabasePlugin alloc] init]; | 
|  | instance.channel = channel; | 
|  | [registrar addMethodCallDelegate:instance channel:channel]; | 
|  | } | 
|  |  | 
|  | - (instancetype)init { | 
|  | self = [super init]; | 
|  | if (self) { | 
|  | if (![FIRApp defaultApp]) { | 
|  | [FIRApp configure]; | 
|  | } | 
|  | self.updatedSnapshots = [NSMutableDictionary new]; | 
|  | } | 
|  | return self; | 
|  | } | 
|  |  | 
|  | - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { | 
|  | FIRDatabase *database; | 
|  | NSString *appName = call.arguments[@"app"]; | 
|  | NSString *databaseURL = call.arguments[@"databaseURL"]; | 
|  | if (![appName isEqual:[NSNull null]] && ![databaseURL isEqual:[NSNull null]]) { | 
|  | database = [FIRDatabase databaseForApp:[FIRApp appNamed:appName] URL:databaseURL]; | 
|  | } else if (![appName isEqual:[NSNull null]]) { | 
|  | database = [FIRDatabase databaseForApp:[FIRApp appNamed:appName]]; | 
|  | } else if (![databaseURL isEqual:[NSNull null]]) { | 
|  | database = [FIRDatabase databaseWithURL:databaseURL]; | 
|  | } else { | 
|  | database = [FIRDatabase database]; | 
|  | } | 
|  | void (^defaultCompletionBlock)(NSError *, FIRDatabaseReference *) = | 
|  | ^(NSError *error, FIRDatabaseReference *ref) { | 
|  | result(error.flutterError); | 
|  | }; | 
|  | if ([@"FirebaseDatabase#goOnline" isEqualToString:call.method]) { | 
|  | [database goOnline]; | 
|  | result(nil); | 
|  | } else if ([@"FirebaseDatabase#goOffline" isEqualToString:call.method]) { | 
|  | [database goOffline]; | 
|  | result(nil); | 
|  | } else if ([@"FirebaseDatabase#purgeOutstandingWrites" isEqualToString:call.method]) { | 
|  | [database purgeOutstandingWrites]; | 
|  | result(nil); | 
|  | } else if ([@"FirebaseDatabase#setPersistenceEnabled" isEqualToString:call.method]) { | 
|  | NSNumber *value = call.arguments[@"enabled"]; | 
|  | @try { | 
|  | database.persistenceEnabled = value.boolValue; | 
|  | result([NSNumber numberWithBool:YES]); | 
|  | } @catch (NSException *exception) { | 
|  | if ([@"FIRDatabaseAlreadyInUse" isEqualToString:exception.name]) { | 
|  | // Database is already in use, e.g. after hot reload/restart. | 
|  | result([NSNumber numberWithBool:NO]); | 
|  | } else { | 
|  | @throw; | 
|  | } | 
|  | } | 
|  | } else if ([@"FirebaseDatabase#setPersistenceCacheSizeBytes" isEqualToString:call.method]) { | 
|  | NSNumber *value = call.arguments[@"cacheSize"]; | 
|  | @try { | 
|  | database.persistenceCacheSizeBytes = value.unsignedIntegerValue; | 
|  | result([NSNumber numberWithBool:YES]); | 
|  | } @catch (NSException *exception) { | 
|  | if ([@"FIRDatabaseAlreadyInUse" isEqualToString:exception.name]) { | 
|  | // Database is already in use, e.g. after hot reload/restart. | 
|  | result([NSNumber numberWithBool:NO]); | 
|  | } else { | 
|  | @throw; | 
|  | } | 
|  | } | 
|  | } else if ([@"DatabaseReference#set" isEqualToString:call.method]) { | 
|  | [getReference(database, call.arguments) setValue:call.arguments[@"value"] | 
|  | andPriority:call.arguments[@"priority"] | 
|  | withCompletionBlock:defaultCompletionBlock]; | 
|  | } else if ([@"DatabaseReference#update" isEqualToString:call.method]) { | 
|  | [getReference(database, call.arguments) updateChildValues:call.arguments[@"value"] | 
|  | withCompletionBlock:defaultCompletionBlock]; | 
|  | } else if ([@"DatabaseReference#setPriority" isEqualToString:call.method]) { | 
|  | [getReference(database, call.arguments) setPriority:call.arguments[@"priority"] | 
|  | withCompletionBlock:defaultCompletionBlock]; | 
|  | } else if ([@"DatabaseReference#runTransaction" isEqualToString:call.method]) { | 
|  | [getReference(database, call.arguments) | 
|  | runTransactionBlock:^FIRTransactionResult *_Nonnull(FIRMutableData *_Nonnull currentData) { | 
|  | // Create semaphore to allow native side to wait while snapshot | 
|  | // updates occur on the Dart side. | 
|  | dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); | 
|  |  | 
|  | NSObject *snapshot = | 
|  | @{@"key" : currentData.key ?: [NSNull null], @"value" : currentData.value}; | 
|  |  | 
|  | __block bool shouldAbort = false; | 
|  |  | 
|  | [self.channel invokeMethod:@"DoTransaction" | 
|  | arguments:@{ | 
|  | @"transactionKey" : call.arguments[@"transactionKey"], | 
|  | @"snapshot" : snapshot | 
|  | } | 
|  | result:^(id _Nullable result) { | 
|  | if ([result isKindOfClass:[FlutterError class]]) { | 
|  | FlutterError *flutterError = ((FlutterError *)result); | 
|  | NSLog(@"Error code: %@", flutterError.code); | 
|  | NSLog(@"Error message: %@", flutterError.message); | 
|  | NSLog(@"Error details: %@", flutterError.details); | 
|  | shouldAbort = true; | 
|  | } else if ([result isEqual:FlutterMethodNotImplemented]) { | 
|  | NSLog(@"DoTransaction not implemented on the Dart side."); | 
|  | shouldAbort = true; | 
|  | } else { | 
|  | [self.updatedSnapshots | 
|  | setObject:result | 
|  | forKey:call.arguments[@"transactionKey"]]; | 
|  | } | 
|  | dispatch_semaphore_signal(semaphore); | 
|  | }]; | 
|  |  | 
|  | // Wait while Dart side updates the snapshot. Incoming transactionTimeout is in | 
|  | // milliseconds so converting to nanoseconds for use with dispatch_semaphore_wait. | 
|  | long result = dispatch_semaphore_wait( | 
|  | semaphore, | 
|  | dispatch_time(DISPATCH_TIME_NOW, | 
|  | [call.arguments[@"transactionTimeout"] integerValue] * 1000000)); | 
|  |  | 
|  | if (result == 0 && !shouldAbort) { | 
|  | // Set FIRMutableData value to value returned from the Dart side. | 
|  | currentData.value = | 
|  | [self.updatedSnapshots objectForKey:call.arguments[@"transactionKey"]][@"value"]; | 
|  | } else { | 
|  | if (result != 0) { | 
|  | NSLog(@"Transaction at %@ timed out.", [getReference(database, call.arguments) URL]); | 
|  | } | 
|  | return [FIRTransactionResult abort]; | 
|  | } | 
|  |  | 
|  | return [FIRTransactionResult successWithValue:currentData]; | 
|  | } | 
|  | andCompletionBlock:^(NSError *_Nullable error, BOOL committed, | 
|  | FIRDataSnapshot *_Nullable snapshot) { | 
|  | // Invoke transaction completion on the Dart side. | 
|  | result(@{ | 
|  | @"transactionKey" : call.arguments[@"transactionKey"], | 
|  | @"error" : error.dictionary ?: [NSNull null], | 
|  | @"committed" : [NSNumber numberWithBool:committed], | 
|  | @"snapshot" : @{@"key" : snapshot.key ?: [NSNull null], @"value" : snapshot.value} | 
|  | }); | 
|  | }]; | 
|  | } else if ([@"OnDisconnect#set" isEqualToString:call.method]) { | 
|  | [getReference(database, call.arguments) onDisconnectSetValue:call.arguments[@"value"] | 
|  | andPriority:call.arguments[@"priority"] | 
|  | withCompletionBlock:defaultCompletionBlock]; | 
|  | } else if ([@"OnDisconnect#update" isEqualToString:call.method]) { | 
|  | [getReference(database, call.arguments) onDisconnectUpdateChildValues:call.arguments[@"value"] | 
|  | withCompletionBlock:defaultCompletionBlock]; | 
|  | } else if ([@"OnDisconnect#cancel" isEqualToString:call.method]) { | 
|  | [getReference(database, call.arguments) | 
|  | cancelDisconnectOperationsWithCompletionBlock:defaultCompletionBlock]; | 
|  | } else if ([@"Query#observe" isEqualToString:call.method]) { | 
|  | FIRDataEventType eventType = parseEventType(call.arguments[@"eventType"]); | 
|  | __block FIRDatabaseHandle handle = [getDatabaseQuery(database, call.arguments) | 
|  | observeEventType:eventType | 
|  | andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *previousSiblingKey) { | 
|  | [self.channel invokeMethod:@"Event" | 
|  | arguments:@{ | 
|  | @"handle" : [NSNumber numberWithUnsignedInteger:handle], | 
|  | @"snapshot" : @{ | 
|  | @"key" : snapshot.key ?: [NSNull null], | 
|  | @"value" : roundDoubles(snapshot.value) ?: [NSNull null], | 
|  | }, | 
|  | @"previousSiblingKey" : previousSiblingKey ?: [NSNull null], | 
|  | }]; | 
|  | } | 
|  | withCancelBlock:^(NSError *error) { | 
|  | [self.channel invokeMethod:@"Error" | 
|  | arguments:@{ | 
|  | @"handle" : [NSNumber numberWithUnsignedInteger:handle], | 
|  | @"error" : error.dictionary, | 
|  | }]; | 
|  | }]; | 
|  | result([NSNumber numberWithUnsignedInteger:handle]); | 
|  | } else if ([@"Query#removeObserver" isEqualToString:call.method]) { | 
|  | FIRDatabaseHandle handle = [call.arguments[@"handle"] unsignedIntegerValue]; | 
|  | [getDatabaseQuery(database, call.arguments) removeObserverWithHandle:handle]; | 
|  | result(nil); | 
|  | } else if ([@"Query#keepSynced" isEqualToString:call.method]) { | 
|  | NSNumber *value = call.arguments[@"value"]; | 
|  | [getDatabaseQuery(database, call.arguments) keepSynced:value.boolValue]; | 
|  | result(nil); | 
|  | } else { | 
|  | result(FlutterMethodNotImplemented); | 
|  | } | 
|  | } | 
|  |  | 
|  | @end |