diff --git a/firebase-firestore/CHANGELOG.md b/firebase-firestore/CHANGELOG.md index 7daaf38a09c..8844874adf8 100644 --- a/firebase-firestore/CHANGELOG.md +++ b/firebase-firestore/CHANGELOG.md @@ -9,6 +9,9 @@ [#7388](//github.com/firebase/firebase-android-sdk/pull/7388) - [changed] Improve query performance by using an unsorted HashMap instead of a sorted TreeMap. [#7389](//github.com/firebase/firebase-android-sdk/pull/7389) +- [changed] Add `java.time.Instant` support to `DocumentSnapshot.toObject()`, + `DocumentReference.set()`, `DocumentReference.update()`, and similar. + [#6235](//github.com/firebase/firebase-android-sdk/pull/6235) # 26.0.1 diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/CustomClassMapper.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/CustomClassMapper.java index 6e0df1e6d4a..63d82821d82 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/CustomClassMapper.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/CustomClassMapper.java @@ -18,6 +18,8 @@ import static com.google.firebase.firestore.util.ApiUtil.newInstance; import android.net.Uri; +import android.os.Build; +import androidx.annotation.RequiresApi; import com.google.firebase.Timestamp; import com.google.firebase.firestore.Blob; import com.google.firebase.firestore.DocumentId; @@ -42,6 +44,7 @@ import java.lang.reflect.WildcardType; import java.net.URI; import java.net.URL; +import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -177,6 +180,9 @@ private static Object serialize(T o, ErrorPath path) { || o instanceof FieldValue || o instanceof VectorValue) { return o; + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && o instanceof Instant) { + Instant instant = (Instant) o; + return new Timestamp(instant.getEpochSecond(), instant.getNano()); } else if (o instanceof Uri || o instanceof URI || o instanceof URL) { return o.toString(); } else { @@ -237,6 +243,9 @@ private static T deserializeToClass(Object o, Class clazz, DeserializeCon return (T) convertDate(o, context); } else if (Timestamp.class.isAssignableFrom(clazz)) { return (T) convertTimestamp(o, context); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + && Instant.class.isAssignableFrom(clazz)) { + return (T) convertInstant(o, context); } else if (Blob.class.isAssignableFrom(clazz)) { return (T) convertBlob(o, context); } else if (GeoPoint.class.isAssignableFrom(clazz)) { @@ -512,6 +521,20 @@ private static Timestamp convertTimestamp(Object o, DeserializeContext context) } } + @RequiresApi(api = Build.VERSION_CODES.O) + private static Instant convertInstant(Object o, DeserializeContext context) { + if (o instanceof Timestamp) { + Timestamp timestamp = (Timestamp) o; + return Instant.ofEpochSecond(timestamp.getSeconds(), timestamp.getNanoseconds()); + } else if (o instanceof Date) { + return Instant.ofEpochMilli(((Date) o).getTime()); + } else { + throw deserializeError( + context.errorPath, + "Failed to convert value of type " + o.getClass().getName() + " to Instant"); + } + } + private static Blob convertBlob(Object o, DeserializeContext context) { if (o instanceof Blob) { return (Blob) o; @@ -933,13 +956,15 @@ Map serialize(T object, ErrorPath path) { private void applyFieldAnnotations(Field field) { if (field.isAnnotationPresent(ServerTimestamp.class)) { Class fieldType = field.getType(); - if (fieldType != Date.class && fieldType != Timestamp.class) { + if (fieldType != Date.class + && fieldType != Timestamp.class + && !(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && fieldType == Instant.class)) { throw new IllegalArgumentException( "Field " + field.getName() + " is annotated with @ServerTimestamp but is " + fieldType - + " instead of Date or Timestamp."); + + " instead of Date, Timestamp, or Instant."); } serverTimestamps.add(propertyName(field)); } @@ -954,13 +979,15 @@ private void applyFieldAnnotations(Field field) { private void applyGetterAnnotations(Method method) { if (method.isAnnotationPresent(ServerTimestamp.class)) { Class returnType = method.getReturnType(); - if (returnType != Date.class && returnType != Timestamp.class) { + if (returnType != Date.class + && returnType != Timestamp.class + && !(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && returnType == Instant.class)) { throw new IllegalArgumentException( "Method " + method.getName() + " is annotated with @ServerTimestamp but returns " + returnType - + " instead of Date or Timestamp."); + + " instead of Date, Timestamp, or Instant."); } serverTimestamps.add(propertyName(method)); } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/util/MapperTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/util/MapperTest.java index bab4691f6f5..e9b478de7c1 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/util/MapperTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/util/MapperTest.java @@ -22,6 +22,7 @@ import static org.junit.Assert.fail; import androidx.annotation.Nullable; +import com.google.firebase.Timestamp; import com.google.firebase.firestore.DocumentId; import com.google.firebase.firestore.DocumentReference; import com.google.firebase.firestore.Exclude; @@ -29,6 +30,7 @@ import com.google.firebase.firestore.TestUtil; import com.google.firebase.firestore.ThrowOnExtraProperties; import java.io.Serializable; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -37,6 +39,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import org.junit.Test; import org.robolectric.annotation.Config; @@ -95,6 +98,40 @@ public boolean isValue() { } } + private static class TimeBean { + public Timestamp timestamp; + public Date date; + public Instant instant; + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + TimeBean timeBean = (TimeBean) o; + return Objects.equals(timestamp, timeBean.timestamp) + && Objects.equals(date, timeBean.date) + && Objects.equals(instant, timeBean.instant); + } + + @Override + public int hashCode() { + return Objects.hash(timestamp, date, instant); + } + + @Override + public String toString() { + return "TimeBean{" + + "_date=" + + date + + ", _timestamp=" + + timestamp + + ", _instant=" + + instant + + '}'; + } + } + private static class ShortBean { private short value; @@ -1476,6 +1513,30 @@ public void serializeBooleanBean() { assertJson("{'value': true}", serialize(bean)); } + @Test + public void serializeTimeBean() { + TimeBean bean = new TimeBean(); + bean.instant = Instant.ofEpochSecond(1234, 5678); + bean.timestamp = new Timestamp(bean.instant); + bean.date = new Date(1234); + assertEquals( + Map.of("timestamp", bean.timestamp, "date", bean.date, "instant", bean.timestamp), + serialize(bean)); + } + + @Test + public void deserializeTimeBean() { + TimeBean bean = new TimeBean(); + bean.instant = Instant.ofEpochSecond(1234, 5678); + bean.timestamp = new Timestamp(bean.instant); + bean.date = new Date(1234); + assertEquals( + bean, + convertToCustomClass( + Map.of("timestamp", bean.timestamp, "date", bean.date, "instant", bean.timestamp), + TimeBean.class)); + } + @Test public void serializeFloatBean() { FloatBean bean = new FloatBean();