Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -177,6 +180,9 @@ private static <T> 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 {
Expand Down Expand Up @@ -237,6 +243,9 @@ private static <T> T deserializeToClass(Object o, Class<T> 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)) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -933,13 +956,15 @@ Map<String, Object> 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));
}
Expand All @@ -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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@
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;
import com.google.firebase.firestore.PropertyName;
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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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();
Expand Down
Loading