blob: 714094683181f52f8e89205629811c2b5b9e0594 [file] [log] [blame]
// 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