| // 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> |
| |
| static FlutterError *getFlutterError(NSError *error) { |
| if (error == nil) return nil; |
| |
| return [FlutterError errorWithCode:[NSString stringWithFormat:@"Error %ld", error.code] |
| message:error.domain |
| details:error.localizedDescription]; |
| } |
| |
| static NSDictionary *getDictionaryFromError(NSError *error) { |
| if (!error) { |
| return nil; |
| } |
| return @{ |
| @"code" : @(error.code), |
| @"message" : error.domain ?: [NSNull null], |
| @"details" : error.localizedDescription ?: [NSNull null], |
| }; |
| } |
| |
| @interface FLTFirebaseDatabasePlugin () |
| |
| @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 appNamed:@"__FIRAPP_DEFAULT"]) { |
| NSLog(@"Configuring the default Firebase app..."); |
| [FIRApp configure]; |
| NSLog(@"Configured the default Firebase app %@.", [FIRApp defaultApp].name); |
| } |
| 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(getFlutterError(error)); |
| }; |
| 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" : getDictionaryFromError(error) ?: [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" : getDictionaryFromError(error), |
| }]; |
| }]; |
| 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 |