diff --git a/packages/cloud_firestore/CHANGELOG.md b/packages/cloud_firestore/CHANGELOG.md index 1618be4376be..ba5e0613b6e0 100644 --- a/packages/cloud_firestore/CHANGELOG.md +++ b/packages/cloud_firestore/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.8.2 + +* Added `Firestore.settings` +* Added `Timestamp` class + ## 0.8.1+1 * Bump Android dependencies to latest. diff --git a/packages/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/cloudfirestore/CloudFirestorePlugin.java b/packages/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/cloudfirestore/CloudFirestorePlugin.java index bdc156b37ca6..2f29704e0db2 100644 --- a/packages/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/cloudfirestore/CloudFirestorePlugin.java +++ b/packages/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/cloudfirestore/CloudFirestorePlugin.java @@ -14,6 +14,7 @@ import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; +import com.google.firebase.Timestamp; import com.google.firebase.firestore.Blob; import com.google.firebase.firestore.CollectionReference; import com.google.firebase.firestore.DocumentChange; @@ -571,6 +572,33 @@ public void onFailure(@NonNull Exception e) { Boolean enable = (Boolean) arguments.get("enable"); FirebaseFirestoreSettings.Builder builder = new FirebaseFirestoreSettings.Builder(); builder.setPersistenceEnabled(enable); + FirebaseFirestoreSettings settings = builder.build(); + getFirestore(arguments).setFirestoreSettings(settings); + result.success(null); + break; + } + case "Firestore#settings": + { + final Map arguments = call.arguments(); + final FirebaseFirestoreSettings.Builder builder = new FirebaseFirestoreSettings.Builder(); + + if (arguments.get("persistenceEnabled") != null) { + builder.setPersistenceEnabled((Boolean) arguments.get("persistenceEnabled")); + } + + if (arguments.get("host") != null) { + builder.setHost((String) arguments.get("host")); + } + + if (arguments.get("sslEnabled") != null) { + builder.setSslEnabled((Boolean) arguments.get("sslEnabled")); + } + + if (arguments.get("timestampsInSnapshotsEnabled") != null) { + builder.setTimestampsInSnapshotsEnabled( + (Boolean) arguments.get("timestampsInSnapshotsEnabled")); + } + FirebaseFirestoreSettings settings = builder.build(); getFirestore(arguments).setFirestoreSettings(settings); result.success(null); @@ -596,12 +624,17 @@ final class FirestoreMessageCodec extends StandardMessageCodec { private static final byte ARRAY_REMOVE = (byte) 133; private static final byte DELETE = (byte) 134; private static final byte SERVER_TIMESTAMP = (byte) 135; + private static final byte TIMESTAMP = (byte) 136; @Override protected void writeValue(ByteArrayOutputStream stream, Object value) { if (value instanceof Date) { stream.write(DATE_TIME); writeLong(stream, ((Date) value).getTime()); + } else if (value instanceof Timestamp) { + stream.write(TIMESTAMP); + writeLong(stream, ((Timestamp) value).getSeconds()); + writeInt(stream, ((Timestamp) value).getNanoseconds()); } else if (value instanceof GeoPoint) { stream.write(GEO_POINT); writeAlignment(stream, 8); @@ -625,6 +658,8 @@ protected Object readValueOfType(byte type, ByteBuffer buffer) { switch (type) { case DATE_TIME: return new Date(buffer.getLong()); + case TIMESTAMP: + return new Timestamp(buffer.getLong(), buffer.getInt()); case GEO_POINT: readAlignment(buffer, 8); return new GeoPoint(buffer.getDouble(), buffer.getDouble()); diff --git a/packages/cloud_firestore/example/lib/main.dart b/packages/cloud_firestore/example/lib/main.dart index e2be166e7ed4..7fde45e1edeb 100755 --- a/packages/cloud_firestore/example/lib/main.dart +++ b/packages/cloud_firestore/example/lib/main.dart @@ -19,6 +19,7 @@ Future main() async { ), ); final Firestore firestore = Firestore(app: app); + await firestore.settings(timestampsInSnapshotsEnabled: true); runApp(MaterialApp( title: 'Firestore Example', home: MyHomePage(firestore: firestore))); @@ -57,9 +58,9 @@ class MyHomePage extends StatelessWidget { CollectionReference get messages => firestore.collection('messages'); Future _addMessage() async { - final DocumentReference document = messages.document(); - document.setData({ + await messages.add({ 'message': 'Hello world!', + 'created_at': FieldValue.serverTimestamp(), }); } diff --git a/packages/cloud_firestore/ios/Classes/CloudFirestorePlugin.m b/packages/cloud_firestore/ios/Classes/CloudFirestorePlugin.m index 2bd912032d77..1d9f0ea20bab 100644 --- a/packages/cloud_firestore/ios/Classes/CloudFirestorePlugin.m +++ b/packages/cloud_firestore/ios/Classes/CloudFirestorePlugin.m @@ -133,6 +133,7 @@ - (FlutterError *)flutterError { const UInt8 ARRAY_REMOVE = 133; const UInt8 DELETE = 134; const UInt8 SERVER_TIMESTAMP = 135; +const UInt8 TIMESTAMP = 136; @interface FirestoreWriter : FlutterStandardWriter - (void)writeValue:(id)value; @@ -146,6 +147,13 @@ - (void)writeValue:(id)value { NSTimeInterval time = date.timeIntervalSince1970; SInt64 ms = (SInt64)(time * 1000.0); [self writeBytes:&ms length:8]; + } else if ([value isKindOfClass:[FIRTimestamp class]]) { + FIRTimestamp *timestamp = value; + SInt64 seconds = timestamp.seconds; + int nanoseconds = timestamp.nanoseconds; + [self writeByte:TIMESTAMP]; + [self writeBytes:(UInt8 *)&seconds length:8]; + [self writeBytes:(UInt8 *)&nanoseconds length:4]; } else if ([value isKindOfClass:[FIRGeoPoint class]]) { FIRGeoPoint *geoPoint = value; Float64 latitude = geoPoint.latitude; @@ -184,6 +192,13 @@ - (id)readValueOfType:(UInt8)type { NSTimeInterval time = [NSNumber numberWithLong:value].doubleValue / 1000.0; return [NSDate dateWithTimeIntervalSince1970:time]; } + case TIMESTAMP: { + SInt64 seconds; + int nanoseconds; + [self readBytes:&seconds length:8]; + [self readBytes:&nanoseconds length:4]; + return [[FIRTimestamp alloc] initWithSeconds:seconds nanoseconds:nanoseconds]; + } case GEO_POINT: { Float64 latitude; Float64 longitude; @@ -482,6 +497,23 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result FIRFirestore *db = getFirestore(call.arguments); db.settings = settings; result(nil); + } else if ([@"Firestore#settings" isEqualToString:call.method]) { + FIRFirestoreSettings *settings = [[FIRFirestoreSettings alloc] init]; + if (![call.arguments[@"persistenceEnabled"] isEqual:[NSNull null]]) { + settings.persistenceEnabled = (bool)call.arguments[@"persistenceEnabled"]; + } + if (![call.arguments[@"host"] isEqual:[NSNull null]]) { + settings.host = (NSString *)call.arguments[@"host"]; + } + if (![call.arguments[@"sslEnabled"] isEqual:[NSNull null]]) { + settings.sslEnabled = (bool)call.arguments[@"sslEnabled"]; + } + if (![call.arguments[@"timestampsInSnapshotsEnabled"] isEqual:[NSNull null]]) { + settings.timestampsInSnapshotsEnabled = (bool)call.arguments[@"timestampsInSnapshotsEnabled"]; + } + FIRFirestore *db = getFirestore(call.arguments); + db.settings = settings; + result(nil); } else { result(FlutterMethodNotImplemented); } diff --git a/packages/cloud_firestore/lib/cloud_firestore.dart b/packages/cloud_firestore/lib/cloud_firestore.dart index c7d3cb29128b..35f81c5a2c09 100755 --- a/packages/cloud_firestore/lib/cloud_firestore.dart +++ b/packages/cloud_firestore/lib/cloud_firestore.dart @@ -29,5 +29,6 @@ part 'src/geo_point.dart'; part 'src/query.dart'; part 'src/query_snapshot.dart'; part 'src/snapshot_metadata.dart'; +part 'src/timestamp.dart'; part 'src/transaction.dart'; part 'src/write_batch.dart'; diff --git a/packages/cloud_firestore/lib/src/firestore.dart b/packages/cloud_firestore/lib/src/firestore.dart index 85f15a50b19f..7af87dff32eb 100644 --- a/packages/cloud_firestore/lib/src/firestore.dart +++ b/packages/cloud_firestore/lib/src/firestore.dart @@ -119,6 +119,7 @@ class Firestore { return result?.cast() ?? {}; } + @deprecated Future enablePersistence(bool enable) async { assert(enable != null); await channel.invokeMethod('Firestore#enablePersistence', { @@ -126,4 +127,18 @@ class Firestore { 'enable': enable, }); } + + Future settings( + {bool persistenceEnabled, + String host, + bool sslEnabled, + bool timestampsInSnapshotsEnabled}) async { + await channel.invokeMethod('Firestore#settings', { + 'app': app.name, + 'persistenceEnabled': persistenceEnabled, + 'host': host, + 'sslEnabled': sslEnabled, + 'timestampsInSnapshotsEnabled': timestampsInSnapshotsEnabled, + }); + } } diff --git a/packages/cloud_firestore/lib/src/firestore_message_codec.dart b/packages/cloud_firestore/lib/src/firestore_message_codec.dart index 3bf93db46d79..0708fa5c5f32 100644 --- a/packages/cloud_firestore/lib/src/firestore_message_codec.dart +++ b/packages/cloud_firestore/lib/src/firestore_message_codec.dart @@ -16,6 +16,7 @@ class FirestoreMessageCodec extends StandardMessageCodec { static const int _kArrayRemove = 133; static const int _kDelete = 134; static const int _kServerTimestamp = 135; + static const int _kTimestamp = 136; static const Map _kFieldValueCodes = { @@ -30,6 +31,10 @@ class FirestoreMessageCodec extends StandardMessageCodec { if (value is DateTime) { buffer.putUint8(_kDateTime); buffer.putInt64(value.millisecondsSinceEpoch); + } else if (value is Timestamp) { + buffer.putUint8(_kTimestamp); + buffer.putInt64(value.seconds); + buffer.putInt32(value.nanoseconds); } else if (value is GeoPoint) { buffer.putUint8(_kGeoPoint); buffer.putFloat64(value.latitude); @@ -61,6 +66,8 @@ class FirestoreMessageCodec extends StandardMessageCodec { switch (type) { case _kDateTime: return DateTime.fromMillisecondsSinceEpoch(buffer.getInt64()); + case _kTimestamp: + return Timestamp(buffer.getInt64(), buffer.getInt32()); case _kGeoPoint: return GeoPoint(buffer.getFloat64(), buffer.getFloat64()); case _kDocumentReference: diff --git a/packages/cloud_firestore/lib/src/timestamp.dart b/packages/cloud_firestore/lib/src/timestamp.dart new file mode 100644 index 000000000000..027f5a55ed76 --- /dev/null +++ b/packages/cloud_firestore/lib/src/timestamp.dart @@ -0,0 +1,98 @@ +// Copyright 2018, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +part of cloud_firestore; + +const int _kThousand = 1000; +const int _kMillion = 1000000; +const int _kBillion = 1000000000; + +void _check(bool expr, String name, int value) { + if (!expr) { + throw ArgumentError("Timestamp $name out of range: $value"); + } +} + +/// A Timestamp represents a point in time independent of any time zone or calendar, +/// represented as seconds and fractions of seconds at nanosecond resolution in UTC +/// Epoch time. It is encoded using the Proleptic Gregorian Calendar which extends +/// the Gregorian calendar backwards to year one. It is encoded assuming all minutes +/// are 60 seconds long, i.e. leap seconds are "smeared" so that no leap second table +/// is needed for interpretation. Range is from 0001-01-01T00:00:00Z to +/// 9999-12-31T23:59:59.999999999Z. By restricting to that range, we ensure that we +/// can convert to and from RFC 3339 date strings. +/// +/// For more information, see [the reference timestamp definition](https://github.com/google/protobuf/blob/master/src/google/protobuf/timestamp.proto) +class Timestamp implements Comparable { + Timestamp(this._seconds, this._nanoseconds) { + _validateRange(_seconds, _nanoseconds); + } + + factory Timestamp.fromMillisecondsSinceEpoch(int milliseconds) { + final int seconds = (milliseconds / _kThousand).floor(); + final int nanoseconds = (milliseconds - seconds * _kThousand) * _kMillion; + return Timestamp(seconds, nanoseconds); + } + + factory Timestamp.fromMicrosecondsSinceEpoch(int microseconds) { + final int seconds = (microseconds / _kMillion).floor(); + final int nanoseconds = (microseconds - seconds * _kMillion) * _kThousand; + return Timestamp(seconds, nanoseconds); + } + + factory Timestamp.fromDate(DateTime date) { + return Timestamp.fromMicrosecondsSinceEpoch(date.microsecondsSinceEpoch); + } + + factory Timestamp.now() { + return Timestamp.fromMicrosecondsSinceEpoch( + DateTime.now().microsecondsSinceEpoch); + } + + final int _seconds; + final int _nanoseconds; + + static const int _kStartOfTime = -62135596800; + static const int _kEndOfTime = 253402300800; + + int get seconds => _seconds; + + int get nanoseconds => _nanoseconds; + + int get millisecondsSinceEpoch => + (seconds * _kThousand + nanoseconds / _kMillion).floor(); + + int get microsecondsSinceEpoch => + (seconds * _kMillion + nanoseconds / _kThousand).floor(); + + DateTime toDate() { + return DateTime.fromMicrosecondsSinceEpoch(microsecondsSinceEpoch); + } + + @override + int get hashCode => hashValues(seconds, nanoseconds); + @override + bool operator ==(dynamic o) => + o is Timestamp && o.seconds == seconds && o.nanoseconds == nanoseconds; + @override + int compareTo(Timestamp other) { + if (seconds == other.seconds) { + return nanoseconds.compareTo(other.nanoseconds); + } + + return seconds.compareTo(other.seconds); + } + + @override + String toString() { + return "Timestamp(seconds=$seconds, nanoseconds=$nanoseconds)"; + } + + static void _validateRange(int seconds, int nanoseconds) { + _check(nanoseconds >= 0, 'nanoseconds', nanoseconds); + _check(nanoseconds < _kBillion, 'nanoseconds', nanoseconds); + _check(seconds >= _kStartOfTime, 'seconds', seconds); + _check(seconds < _kEndOfTime, 'seconds', seconds); + } +} diff --git a/packages/cloud_firestore/pubspec.yaml b/packages/cloud_firestore/pubspec.yaml index 37666dad601b..e7e3b3bef2d7 100755 --- a/packages/cloud_firestore/pubspec.yaml +++ b/packages/cloud_firestore/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Cloud Firestore, a cloud-hosted, noSQL database live synchronization and offline support on Android and iOS. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/cloud_firestore -version: 0.8.1+1 +version: 0.8.2 flutter: plugin: diff --git a/packages/cloud_firestore/test/cloud_firestore_test.dart b/packages/cloud_firestore/test/cloud_firestore_test.dart index 99232756f077..f0c9828a0d8b 100755 --- a/packages/cloud_firestore/test/cloud_firestore_test.dart +++ b/packages/cloud_firestore/test/cloud_firestore_test.dart @@ -559,8 +559,10 @@ void main() { group('FirestoreMessageCodec', () { const MessageCodec codec = FirestoreMessageCodec(); final DateTime testTime = DateTime(2015, 10, 30, 11, 16); + final Timestamp timestamp = Timestamp.fromDate(testTime); test('should encode and decode simple messages', () { _checkEncodeDecode(codec, testTime); + _checkEncodeDecode(codec, timestamp); _checkEncodeDecode( codec, const GeoPoint(37.421939, -122.083509)); _checkEncodeDecode(codec, firestore.document('foo/bar')); @@ -588,6 +590,78 @@ void main() { }); }); + group('Timestamp', () { + test('is accurate for dates after epoch', () { + final DateTime date = DateTime.fromMillisecondsSinceEpoch(22501); + final Timestamp timestamp = Timestamp.fromDate(date); + + expect(timestamp.seconds, equals(22)); + expect(timestamp.nanoseconds, equals(501000000)); + }); + + test('is accurate for dates before epoch', () { + final DateTime date = DateTime.fromMillisecondsSinceEpoch(-1250); + final Timestamp timestamp = Timestamp.fromDate(date); + + expect(timestamp.seconds, equals(-2)); + expect(timestamp.nanoseconds, equals(750000000)); + }); + + test('creates equivalent timestamps regardless of factory', () { + const int kMilliseconds = 22501; + const int kMicroseconds = 22501000; + final DateTime date = + DateTime.fromMicrosecondsSinceEpoch(kMicroseconds); + + final Timestamp timestamp = Timestamp(22, 501000000); + final Timestamp milliTimestamp = + Timestamp.fromMillisecondsSinceEpoch(kMilliseconds); + final Timestamp microTimestamp = + Timestamp.fromMicrosecondsSinceEpoch(kMicroseconds); + final Timestamp dateTimestamp = Timestamp.fromDate(date); + + expect(timestamp, equals(milliTimestamp)); + expect(milliTimestamp, equals(microTimestamp)); + expect(microTimestamp, equals(dateTimestamp)); + }); + + test('correctly compares timestamps', () { + final Timestamp alpha = Timestamp.fromDate(DateTime(2017, 5, 11)); + final Timestamp beta1 = Timestamp.fromDate(DateTime(2018, 2, 19)); + final Timestamp beta2 = Timestamp.fromDate(DateTime(2018, 4, 2)); + final Timestamp beta3 = Timestamp.fromDate(DateTime(2018, 4, 20)); + final Timestamp preview = Timestamp.fromDate(DateTime(2018, 6, 20)); + final List inOrder = [ + alpha, + beta1, + beta2, + beta3, + preview + ]; + + final List timestamps = [ + beta2, + beta3, + alpha, + preview, + beta1 + ]; + timestamps.sort(); + expect(_deepEqualsList(timestamps, inOrder), isTrue); + }); + + test('rejects dates outside RFC 3339 range', () { + final List invalidDates = [ + DateTime.fromMillisecondsSinceEpoch(-70000000000000), + DateTime.fromMillisecondsSinceEpoch(300000000000000), + ]; + + invalidDates.forEach((DateTime date) { + expect(() => Timestamp.fromDate(date), throwsArgumentError); + }); + }); + }); + group('WriteBatch', () { test('set', () async { final WriteBatch batch = firestore.batch();