Repository: googleapis/java-storage
Pull request #413 (2020-07-01 13:19:14)

feat: add support of customTime metadata
Towards #395 


codecov[bot]: # [Codecov](https://codecov.io/gh/googleapis/java-storage/pull/413?src=pr&el=h1) Report
> Merging [#413](https://codecov.io/gh/googleapis/java-storage/pull/413?src=pr&el=desc) into [master](https://codecov.io/gh/googleapis/java-storage/commit/16036acb25d418704d001b7a4eaeafcaacaf25e7&el=desc) will **increase** coverage by `0.07%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/googleapis/java-storage/pull/413/graphs/tree.svg?width=650&height=150&src=pr&token=ssm139YOdp)](https://codecov.io/gh/googleapis/java-storage/pull/413?src=pr&el=tree)

```diff
@@             Coverage Diff              @@
##             master     #413      +/-   ##
============================================
+ Coverage     63.27%   63.34%   +0.07%     
+ Complexity      618      612       -6     
============================================
  Files            32       32              
  Lines          5113     5124      +11     
  Branches        486      489       +3     
============================================
+ Hits           3235     3246      +11     
  Misses         1713     1713              
  Partials        165      165              
```


| [Impacted Files](https://codecov.io/gh/googleapis/java-storage/pull/413?src=pr&el=tree) | Coverage Δ | Complexity Δ | |
|---|---|---|---|
| [...e/src/main/java/com/google/cloud/storage/Blob.java](https://codecov.io/gh/googleapis/java-storage/pull/413/diff?src=pr&el=tree#diff-Z29vZ2xlLWNsb3VkLXN0b3JhZ2Uvc3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3N0b3JhZ2UvQmxvYi5qYXZh) | `82.38% <100.00%> (+0.20%)` | `30.00 <0.00> (ø)` | |
| [...c/main/java/com/google/cloud/storage/BlobInfo.java](https://codecov.io/gh/googleapis/java-storage/pull/413/diff?src=pr&el=tree#diff-Z29vZ2xlLWNsb3VkLXN0b3JhZ2Uvc3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3N0b3JhZ2UvQmxvYkluZm8uamF2YQ==) | `88.54% <100.00%> (+0.29%)` | `91.00 <1.00> (+3.00)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/googleapis/java-storage/pull/413?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/googleapis/java-storage/pull/413?src=pr&el=footer). Last update [16036ac...70aa315](https://codecov.io/gh/googleapis/java-storage/pull/413?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

athakor: @jkwlui
athakor: @frankyn @jkwlui friendly ping

Patch:
diff --git a/.github/workflows/samples.yaml b/.github/workflows/samples.yaml
new file mode 100644
index 00000000..a1d50073
--- /dev/null
+++ b/.github/workflows/samples.yaml
@@ -0,0 +1,14 @@
+on:
+  pull_request:
+name: samples
+jobs:
+  lint:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/setup-java@v1
+        with:
+          java-version: 8
+      - name: Run checkstyle
+        run: mvn -P lint --quiet --batch-mode checkstyle:check
+        working-directory: samples/snippets
diff --git a/.kokoro/continuous/dependencies.cfg b/.kokoro/continuous/dependencies.cfg
deleted file mode 100644
index a8b9fbf2..00000000
--- a/.kokoro/continuous/dependencies.cfg
+++ /dev/null
@@ -1,12 +0,0 @@
-# Format: //devtools/kokoro/config/proto/build.proto
-
-# Configure the docker image for kokoro-trampoline.
-env_vars: {
-  key: "TRAMPOLINE_IMAGE"
-  value: "gcr.io/cloud-devrel-kokoro-resources/java8"
-}
-
-env_vars: {
-  key: "TRAMPOLINE_BUILD_FILE"
-  value: "github/java-storage/.kokoro/dependencies.sh"
-}
diff --git a/.kokoro/continuous/integration.cfg b/.kokoro/continuous/integration.cfg
deleted file mode 100644
index 3b017fc8..00000000
--- a/.kokoro/continuous/integration.cfg
+++ /dev/null
@@ -1,7 +0,0 @@
-# Format: //devtools/kokoro/config/proto/build.proto
-
-# Configure the docker image for kokoro-trampoline.
-env_vars: {
-  key: "TRAMPOLINE_IMAGE"
-  value: "gcr.io/cloud-devrel-kokoro-resources/java8"
-}
diff --git a/.kokoro/continuous/java11.cfg b/.kokoro/continuous/java11.cfg
deleted file mode 100644
index 709f2b4c..00000000
--- a/.kokoro/continuous/java11.cfg
+++ /dev/null
@@ -1,7 +0,0 @@
-# Format: //devtools/kokoro/config/proto/build.proto
-
-# Configure the docker image for kokoro-trampoline.
-env_vars: {
-  key: "TRAMPOLINE_IMAGE"
-  value: "gcr.io/cloud-devrel-kokoro-resources/java11"
-}
diff --git a/.kokoro/continuous/java7.cfg b/.kokoro/continuous/java7.cfg
deleted file mode 100644
index cb24f44e..00000000
--- a/.kokoro/continuous/java7.cfg
+++ /dev/null
@@ -1,7 +0,0 @@
-# Format: //devtools/kokoro/config/proto/build.proto
-
-# Configure the docker image for kokoro-trampoline.
-env_vars: {
-  key: "TRAMPOLINE_IMAGE"
-  value: "gcr.io/cloud-devrel-kokoro-resources/java7"
-}
diff --git a/.kokoro/continuous/java8-osx.cfg b/.kokoro/continuous/java8-osx.cfg
deleted file mode 100644
index 79fedbfd..00000000
--- a/.kokoro/continuous/java8-osx.cfg
+++ /dev/null
@@ -1,3 +0,0 @@
-# Format: //devtools/kokoro/config/proto/build.proto
-
-build_file: "java-storage/.kokoro/build.sh"
diff --git a/.kokoro/continuous/java8-win.cfg b/.kokoro/continuous/java8-win.cfg
deleted file mode 100644
index 6ae00233..00000000
--- a/.kokoro/continuous/java8-win.cfg
+++ /dev/null
@@ -1,3 +0,0 @@
-# Format: //devtools/kokoro/config/proto/build.proto
-
-build_file: "java-storage/.kokoro/build.bat"
diff --git a/.kokoro/continuous/lint.cfg b/.kokoro/continuous/lint.cfg
deleted file mode 100644
index 6d323c8a..00000000
--- a/.kokoro/continuous/lint.cfg
+++ /dev/null
@@ -1,13 +0,0 @@
-# Format: //devtools/kokoro/config/proto/build.proto
-
-# Configure the docker image for kokoro-trampoline.
-
-env_vars: {
-  key: "TRAMPOLINE_IMAGE"
-  value: "gcr.io/cloud-devrel-kokoro-resources/java8"
-}
-
-env_vars: {
-  key: "JOB_TYPE"
-  value: "lint"
-}
\ No newline at end of file
diff --git a/.kokoro/continuous/propose_release.cfg b/.kokoro/continuous/propose_release.cfg
deleted file mode 100644
index b115c5eb..00000000
--- a/.kokoro/continuous/propose_release.cfg
+++ /dev/null
@@ -1,53 +0,0 @@
-# Format: //devtools/kokoro/config/proto/build.proto
-
-# Build logs will be here
-action {
-  define_artifacts {
-    regex: "**/*sponge_log.xml"
-  }
-}
-
-# Download trampoline resources.
-gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline"
-
-# Use the trampoline script to run in docker.
-build_file: "java-storage/.kokoro/trampoline.sh"
-
-# Configure the docker image for kokoro-trampoline.
-env_vars: {
-  key: "TRAMPOLINE_IMAGE"
-  value: "gcr.io/cloud-devrel-kokoro-resources/node:10-user"
-}
-
-env_vars: {
-  key: "TRAMPOLINE_BUILD_FILE"
-  value: "github/java-storage/.kokoro/continuous/propose_release.sh"
-}
-
-# tokens used by release-please to keep an up-to-date release PR.
-before_action {
-  fetch_keystore {
-    keystore_resource {
-      keystore_config_id: 73713
-      keyname: "github-magic-proxy-key-release-please"
-    }
-  }
-}
-
-before_action {
-  fetch_keystore {
-    keystore_resource {
-      keystore_config_id: 73713
-      keyname: "github-magic-proxy-token-release-please"
-    }
-  }
-}
-
-before_action {
-  fetch_keystore {
-    keystore_resource {
-      keystore_config_id: 73713
-      keyname: "github-magic-proxy-url-release-please"
-    }
-  }
-}
diff --git a/.kokoro/continuous/samples.cfg b/.kokoro/continuous/samples.cfg
deleted file mode 100644
index fa7b493d..00000000
--- a/.kokoro/continuous/samples.cfg
+++ /dev/null
@@ -1,31 +0,0 @@
-# Format: //devtools/kokoro/config/proto/build.proto
-
-# Configure the docker image for kokoro-trampoline.
-env_vars: {
-  key: "TRAMPOLINE_IMAGE"
-  value: "gcr.io/cloud-devrel-kokoro-resources/java8"
-}
-
-env_vars: {
-  key: "JOB_TYPE"
-  value: "samples"
-}
-
-env_vars: {
-    key: "GCLOUD_PROJECT"
-    value: "gcloud-devel"
-}
-
-env_vars: {
-    key: "GOOGLE_APPLICATION_CREDENTIALS"
-    value: "keystore/73713_java_it_service_account"
-}
-
-before_action {
-  fetch_keystore {
-    keystore_resource {
-      keystore_config_id: 73713
-      keyname: "java_it_service_account"
-    }
-  }
-}
diff --git a/.kokoro/dependencies.sh b/.kokoro/dependencies.sh
index cee4f11e..c91e5a56 100755
--- a/.kokoro/dependencies.sh
+++ b/.kokoro/dependencies.sh
@@ -43,12 +43,13 @@ function completenessCheck() {
   # Output dep list with compile scope generated using the original pom
   # Running mvn dependency:list on Java versions that support modules will also include the module of the dependency.
   # This is stripped from the output as it is not present in the flattened pom.
+  # Only dependencies with 'compile' or 'runtime' scope are included from original dependency list.
   msg "Generating dependency list using original pom..."
-  mvn dependency:list -f pom.xml -Dsort=true | grep '\[INFO]    .*:.*:.*:.*:.*' | sed -e s/\\s--\\smodule.*// | grep -v ':test$' >.org-list.txt
+  mvn dependency:list -f pom.xml -DincludeScope=runtime -Dsort=true | grep '\[INFO]    .*:.*:.*:.*:.*' | sed -e s/\\s--\\smodule.*// >.org-list.txt
 
-  # Output dep list generated using the flattened pom (test scope deps are ommitted)
+  # Output dep list generated using the flattened pom (only 'compile' and 'runtime' scopes)
   msg "Generating dependency list using flattened pom..."
-  mvn dependency:list -f .flattened-pom.xml -Dsort=true | grep '\[INFO]    .*:.*:.*:.*:.*' >.new-list.txt
+  mvn dependency:list -f .flattened-pom.xml -DincludeScope=runtime -Dsort=true | grep '\[INFO]    .*:.*:.*:.*:.*' >.new-list.txt
 
   # Compare two dependency lists
   msg "Comparing dependency lists..."
@@ -85,4 +86,4 @@ then
 else
   msg "Errors found. See log statements above."
   exit 1
-fi
+fi
\ No newline at end of file
diff --git a/.kokoro/nightly/dependencies.cfg b/.kokoro/nightly/dependencies.cfg
deleted file mode 100644
index a8b9fbf2..00000000
--- a/.kokoro/nightly/dependencies.cfg
+++ /dev/null
@@ -1,12 +0,0 @@
-# Format: //devtools/kokoro/config/proto/build.proto
-
-# Configure the docker image for kokoro-trampoline.
-env_vars: {
-  key: "TRAMPOLINE_IMAGE"
-  value: "gcr.io/cloud-devrel-kokoro-resources/java8"
-}
-
-env_vars: {
-  key: "TRAMPOLINE_BUILD_FILE"
-  value: "github/java-storage/.kokoro/dependencies.sh"
-}
diff --git a/.kokoro/nightly/lint.cfg b/.kokoro/nightly/lint.cfg
deleted file mode 100644
index 6d323c8a..00000000
--- a/.kokoro/nightly/lint.cfg
+++ /dev/null
@@ -1,13 +0,0 @@
-# Format: //devtools/kokoro/config/proto/build.proto
-
-# Configure the docker image for kokoro-trampoline.
-
-env_vars: {
-  key: "TRAMPOLINE_IMAGE"
-  value: "gcr.io/cloud-devrel-kokoro-resources/java8"
-}
-
-env_vars: {
-  key: "JOB_TYPE"
-  value: "lint"
-}
\ No newline at end of file
diff --git a/.kokoro/release/publish_javadoc.cfg b/.kokoro/release/publish_javadoc.cfg
index 82745e58..c4cb5521 100644
--- a/.kokoro/release/publish_javadoc.cfg
+++ b/.kokoro/release/publish_javadoc.cfg
@@ -1,14 +1,24 @@
 # Format: //devtools/kokoro/config/proto/build.proto
+
+gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/doc-templates/"
+
 env_vars: {
   key: "STAGING_BUCKET"
   value: "docs-staging"
 }
 
+env_vars: {
+  key: "STAGING_BUCKET_V2"
+  value: "docs-staging-v2-staging"
+  # Production will be at: docs-staging-v2
+}
+
 env_vars: {
   key: "TRAMPOLINE_BUILD_FILE"
   value: "github/java-storage/.kokoro/release/publish_javadoc.sh"
 }
 
+
 before_action {
   fetch_keystore {
     keystore_resource {
diff --git a/.kokoro/release/publish_javadoc.sh b/.kokoro/release/publish_javadoc.sh
index 219d37c6..6b592014 100755
--- a/.kokoro/release/publish_javadoc.sh
+++ b/.kokoro/release/publish_javadoc.sh
@@ -24,6 +24,11 @@ if [[ -z "${STAGING_BUCKET}" ]]; then
   exit 1
 fi
 
+if [[ -z "${STAGING_BUCKET_V2}" ]]; then
+  echo "Need to set STAGING_BUCKET_V2 environment variable"
+  exit 1
+fi
+
 # work from the git root directory
 pushd $(dirname "$0")/../../
 
@@ -31,13 +36,13 @@ pushd $(dirname "$0")/../../
 python3 -m pip install gcp-docuploader
 
 # compile all packages
-mvn clean install -B -DskipTests=true
+mvn clean install -B -q -DskipTests=true
 
 NAME=google-cloud-storage
 VERSION=$(grep ${NAME}: versions.txt | cut -d: -f3)
 
 # build the docs
-mvn site -B
+mvn site -B -q
 
 pushd target/site/apidocs
 
@@ -53,3 +58,19 @@ python3 -m docuploader upload . \
   --staging-bucket ${STAGING_BUCKET}
 
 popd
+
+# V2
+mvn clean site -B -q -Ddevsite.template="${KOKORO_GFILE_DIR}/java/"
+
+pushd target/devsite
+
+# create metadata
+python3 -m docuploader create-metadata \
+  --name ${NAME} \
+  --version ${VERSION} \
+  --language java
+
+# upload docs
+python3 -m docuploader upload . \
+  --credentials ${CREDENTIALS} \
+  --staging-bucket ${STAGING_BUCKET_V2}
diff --git a/.kokoro/release/stage.sh b/.kokoro/release/stage.sh
index 3c482cbc..d19191fc 100755
--- a/.kokoro/release/stage.sh
+++ b/.kokoro/release/stage.sh
@@ -16,8 +16,9 @@
 set -eo pipefail
 
 # Start the releasetool reporter
-python3 -m pip install gcp-releasetool
-python3 -m releasetool publish-reporter-script > /tmp/publisher-script; source /tmp/publisher-script
+# Disable reporting due to issue observed with Kokoro blocking releases
+# python3 -m pip install gcp-releasetool
+# python3 -m releasetool publish-reporter-script > /tmp/publisher-script; source /tmp/publisher-script
 
 source $(dirname "$0")/common.sh
 MAVEN_SETTINGS_FILE=$(realpath $(dirname "$0")/../../)/settings.xml
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 39d1c88d..ccbd5094 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -48,7 +48,7 @@ integration tests.
 
 ```bash
 export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service/account.json
-export IT_SERVICE_ACCOUNT_EMAIL=my-service-account@my-project.gserviceaccount.com
+export IT_SERVICE_ACCOUNT_EMAIL=my-service-account@my-project.iam.gserviceaccount.com
 mvn -Penable-integration-tests clean verify
 ```
 
diff --git a/README.md b/README.md
index 000f72b3..349cb168 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ If you are using Maven with [BOM][libraries-bom], add this to your pom.xml file
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>libraries-bom</artifactId>
-      <version>8.1.0</version>
+      <version>9.1.0</version>
       <type>pom</type>
       <scope>import</scope>
     </dependency>
diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml
index ffacd705..6db71ffd 100644
--- a/google-cloud-storage/pom.xml
+++ b/google-cloud-storage/pom.xml
@@ -117,13 +117,13 @@
     <dependency>
       <groupId>com.google.api.grpc</groupId>
       <artifactId>grpc-google-cloud-kms-v1</artifactId>
-      <version>0.86.0</version>
+      <version>0.86.1</version>
       <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>com.google.api.grpc</groupId>
       <artifactId>proto-google-cloud-kms-v1</artifactId>
-      <version>0.86.0</version>
+      <version>0.86.1</version>
       <scope>test</scope>
     </dependency>
     <dependency>
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java
index 679b410a..398eb316 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java
@@ -434,6 +434,12 @@ public class Blob extends BlobInfo {
       return this;
     }
 
+    @Override
+    public Builder setCustomTime(Long customTime) {
+      infoBuilder.setCustomTime(customTime);
+      return this;
+    }
+
     @Override
     Builder setIsDirectory(boolean isDirectory) {
       infoBuilder.setIsDirectory(isDirectory);
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobInfo.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobInfo.java
index ef38ba03..67256d1c 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobInfo.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobInfo.java
@@ -84,6 +84,7 @@ public class BlobInfo implements Serializable {
   private final String etag;
   private final String md5;
   private final String crc32c;
+  private final Long customTime;
   private final String mediaLink;
   private final Map<String, String> metadata;
   private final Long metageneration;
@@ -260,6 +261,26 @@ public class BlobInfo implements Serializable {
      */
     public abstract Builder setCrc32c(String crc32c);
 
+    /**
+     * Sets the custom time for an object. Once set it can't be unset and only changed to a custom
+     * datetime in the future. To unset the custom time, you must either perform a rewrite operation
+     * or upload the data again.
+     *
+     * <p>Example of setting the custom time.
+     *
+     * <pre>{@code
+     * String bucketName = "my-unique-bucket";
+     * String blobName = "my-blob-name";
+     * long customTime = 1598423868301L;
+     * BlobInfo blob = BlobInfo.newBuilder(bucketName, blobName).setCustomTime(customTime).build();
+     * }</pre>
+     */
+    public Builder setCustomTime(Long customTime) {
+      throw new UnsupportedOperationException(
+          "Override setCustomTime with your own implementation,"
+              + " or use com.google.cloud.storage.Blob.");
+    }
+
     /**
      * Sets the CRC32C checksum of blob's data as described in <a
      * href="http://tools.ietf.org/html/rfc4960#appendix-B">RFC 4960, Appendix B;</a> from hex
@@ -325,6 +346,7 @@ public class BlobInfo implements Serializable {
     private String selfLink;
     private String md5;
     private String crc32c;
+    private Long customTime;
     private String mediaLink;
     private Map<String, String> metadata;
     private Long metageneration;
@@ -360,6 +382,7 @@ public class BlobInfo implements Serializable {
       selfLink = blobInfo.selfLink;
       md5 = blobInfo.md5;
       crc32c = blobInfo.crc32c;
+      customTime = blobInfo.customTime;
       mediaLink = blobInfo.mediaLink;
       metadata = blobInfo.metadata;
       metageneration = blobInfo.metageneration;
@@ -488,6 +511,12 @@ public class BlobInfo implements Serializable {
       return this;
     }
 
+    @Override
+    public Builder setCustomTime(Long customTime) {
+      this.customTime = customTime;
+      return this;
+    }
+
     @Override
     public Builder setCrc32cFromHexString(String crc32cHexString) {
       if (crc32cHexString == null) {
@@ -619,6 +648,7 @@ public class BlobInfo implements Serializable {
     selfLink = builder.selfLink;
     md5 = builder.md5;
     crc32c = builder.crc32c;
+    customTime = builder.customTime;
     mediaLink = builder.mediaLink;
     metadata = builder.metadata;
     metageneration = builder.metageneration;
@@ -857,6 +887,11 @@ public class BlobInfo implements Serializable {
     return createTime;
   }
 
+  /** Returns the custom time specified by the user for an object. */
+  public Long getCustomTime() {
+    return customTime;
+  }
+
   /**
    * Returns {@code true} if the current blob represents a directory. This can only happen if the
    * blob is returned by {@link Storage#list(String, Storage.BlobListOption...)} when the {@link
@@ -1002,6 +1037,9 @@ public class BlobInfo implements Serializable {
     if (createTime != null) {
       storageObject.setTimeCreated(new DateTime(createTime));
     }
+    if (customTime != null) {
+      storageObject.setCustomTime(new DateTime(customTime));
+    }
     if (size != null) {
       storageObject.setSize(BigInteger.valueOf(size));
     }
@@ -1125,6 +1163,9 @@ public class BlobInfo implements Serializable {
     if (storageObject.getTimeCreated() != null) {
       builder.setCreateTime(storageObject.getTimeCreated().getValue());
     }
+    if (storageObject.getCustomTime() != null) {
+      builder.setCustomTime(storageObject.getCustomTime().getValue());
+    }
     if (storageObject.getSize() != null) {
       builder.setSize(storageObject.getSize().longValue());
     }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BucketInfo.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BucketInfo.java
index b3e96113..602be08a 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/BucketInfo.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BucketInfo.java
@@ -351,7 +351,11 @@ public class BucketInfo implements Serializable {
           && condition.getAge() == null
           && condition.getCreatedBefore() == null
           && condition.getMatchesStorageClass() == null
-          && condition.getNumberOfNewerVersions() == null) {
+          && condition.getNumberOfNewerVersions() == null
+          && condition.getDaysSinceNoncurrentTime() == null
+          && condition.getNoncurrentTimeBefore() == null
+          && condition.getCustomTimeBefore() == null
+          && condition.getDaysSinceCustomTime() == null) {
         throw new IllegalArgumentException(
             "You must specify at least one condition to use object lifecycle "
                 + "management. Please see https://cloud.google.com/storage/docs/lifecycle for details.");
@@ -419,7 +423,18 @@ public class BucketInfo implements Serializable {
                       ? null
                       : transform(
                           lifecycleCondition.getMatchesStorageClass(),
-                          Functions.toStringFunction()));
+                          Functions.toStringFunction()))
+              .setDaysSinceNoncurrentTime(lifecycleCondition.getDaysSinceNoncurrentTime())
+              .setNoncurrentTimeBefore(
+                  lifecycleCondition.getNoncurrentTimeBefore() == null
+                      ? null
+                      : new DateTime(
+                          true, lifecycleCondition.getNoncurrentTimeBefore().getValue(), 0))
+              .setCustomTimeBefore(
+                  lifecycleCondition.getCustomTimeBefore() == null
+                      ? null
+                      : new DateTime(true, lifecycleCondition.getCustomTimeBefore().getValue(), 0))
+              .setDaysSinceCustomTime(lifecycleCondition.getDaysSinceCustomTime());
 
       rule.setCondition(condition);
 
@@ -462,7 +477,11 @@ public class BucketInfo implements Serializable {
                             public StorageClass apply(String storageClass) {
                               return StorageClass.valueOf(storageClass);
                             }
-                          }));
+                          }))
+              .setDaysSinceNoncurrentTime(condition.getDaysSinceNoncurrentTime())
+              .setNoncurrentTimeBefore(condition.getNoncurrentTimeBefore())
+              .setCustomTimeBefore(condition.getCustomTimeBefore())
+              .setDaysSinceCustomTime(condition.getDaysSinceCustomTime());
 
       return new LifecycleRule(lifecycleAction, conditionBuilder.build());
     }
@@ -480,6 +499,10 @@ public class BucketInfo implements Serializable {
       private final Integer numberOfNewerVersions;
       private final Boolean isLive;
       private final List<StorageClass> matchesStorageClass;
+      private final Integer daysSinceNoncurrentTime;
+      private final DateTime noncurrentTimeBefore;
+      private final DateTime customTimeBefore;
+      private final Integer daysSinceCustomTime;
 
       private LifecycleCondition(Builder builder) {
         this.age = builder.age;
@@ -487,6 +510,10 @@ public class BucketInfo implements Serializable {
         this.numberOfNewerVersions = builder.numberOfNewerVersions;
         this.isLive = builder.isLive;
         this.matchesStorageClass = builder.matchesStorageClass;
+        this.daysSinceNoncurrentTime = builder.daysSinceNoncurrentTime;
+        this.noncurrentTimeBefore = builder.noncurrentTimeBefore;
+        this.customTimeBefore = builder.customTimeBefore;
+        this.daysSinceCustomTime = builder.daysSinceCustomTime;
       }
 
       public Builder toBuilder() {
@@ -495,7 +522,11 @@ public class BucketInfo implements Serializable {
             .setCreatedBefore(this.createdBefore)
             .setNumberOfNewerVersions(this.numberOfNewerVersions)
             .setIsLive(this.isLive)
-            .setMatchesStorageClass(this.matchesStorageClass);
+            .setMatchesStorageClass(this.matchesStorageClass)
+            .setDaysSinceNoncurrentTime(this.daysSinceNoncurrentTime)
+            .setNoncurrentTimeBefore(this.noncurrentTimeBefore)
+            .setCustomTimeBefore(this.customTimeBefore)
+            .setDaysSinceCustomTime(this.daysSinceCustomTime);
       }
 
       public static Builder newBuilder() {
@@ -510,6 +541,10 @@ public class BucketInfo implements Serializable {
             .add("numberofNewerVersions", numberOfNewerVersions)
             .add("isLive", isLive)
             .add("matchesStorageClass", matchesStorageClass)
+            .add("daysSinceNoncurrentTime", daysSinceNoncurrentTime)
+            .add("noncurrentTimeBefore", noncurrentTimeBefore)
+            .add("customTimeBefore", customTimeBefore)
+            .add("daysSinceCustomTime", daysSinceCustomTime)
             .toString();
       }
 
@@ -533,6 +568,28 @@ public class BucketInfo implements Serializable {
         return matchesStorageClass;
       }
 
+      /** Returns the number of days elapsed since the noncurrent timestamp of an object. */
+      public Integer getDaysSinceNoncurrentTime() {
+        return daysSinceNoncurrentTime;
+      }
+
+      /**
+       * Returns the date in RFC 3339 format with only the date part (for instance, "2013-01-15").
+       */
+      public DateTime getNoncurrentTimeBefore() {
+        return noncurrentTimeBefore;
+      }
+
+      /* Returns the date in RFC 3339 format with only the date part (for instance, "2013-01-15").*/
+      public DateTime getCustomTimeBefore() {
+        return customTimeBefore;
+      }
+
+      /** Returns the number of days elapsed since the user-specified timestamp set on an object. */
+      public Integer getDaysSinceCustomTime() {
+        return daysSinceCustomTime;
+      }
+
       /** Builder for {@code LifecycleCondition}. */
       public static class Builder {
         private Integer age;
@@ -540,6 +597,10 @@ public class BucketInfo implements Serializable {
         private Integer numberOfNewerVersions;
         private Boolean isLive;
         private List<StorageClass> matchesStorageClass;
+        private Integer daysSinceNoncurrentTime;
+        private DateTime noncurrentTimeBefore;
+        private DateTime customTimeBefore;
+        private Integer daysSinceCustomTime;
 
         private Builder() {}
 
@@ -594,6 +655,50 @@ public class BucketInfo implements Serializable {
           return this;
         }
 
+        /**
+         * Sets the number of days elapsed since the noncurrent timestamp of an object. The
+         * condition is satisfied if the days elapsed is at least this number. This condition is
+         * relevant only for versioned objects. The value of the field must be a nonnegative
+         * integer. If it's zero, the object version will become eligible for Lifecycle action as
+         * soon as it becomes noncurrent.
+         */
+        public Builder setDaysSinceNoncurrentTime(Integer daysSinceNoncurrentTime) {
+          this.daysSinceNoncurrentTime = daysSinceNoncurrentTime;
+          return this;
+        }
+
+        /**
+         * Sets the date in RFC 3339 format with only the date part (for instance, "2013-01-15").
+         * Note that only date part will be considered, if the time is specified it will be
+         * truncated. This condition is satisfied when the noncurrent time on an object is before
+         * this date. This condition is relevant only for versioned objects.
+         */
+        public Builder setNoncurrentTimeBefore(DateTime noncurrentTimeBefore) {
+          this.noncurrentTimeBefore = noncurrentTimeBefore;
+          return this;
+        }
+
+        /**
+         * Sets the date in RFC 3339 format with only the date part (for instance, "2013-01-15").
+         * Note that only date part will be considered, if the time is specified it will be
+         * truncated. This condition is satisfied when the custom time on an object is before this
+         * date in UTC.
+         */
+        public Builder setCustomTimeBefore(DateTime customTimeBefore) {
+          this.customTimeBefore = customTimeBefore;
+          return this;
+        }
+
+        /**
+         * Sets the number of days elapsed since the user-specified timestamp set on an object. The
+         * condition is satisfied if the days elapsed is at least this number. If no custom
+         * timestamp is specified on an object, the condition does not apply.
+         */
+        public Builder setDaysSinceCustomTime(Integer daysSinceCustomTime) {
+          this.daysSinceCustomTime = daysSinceCustomTime;
+          return this;
+        }
+
         /** Builds a {@code LifecycleCondition} object. * */
         public LifecycleCondition build() {
           return new LifecycleCondition(this);
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/PostPolicyV4.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/PostPolicyV4.java
index cc9b413a..96afca06 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/PostPolicyV4.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/PostPolicyV4.java
@@ -18,56 +18,125 @@ package com.google.cloud.storage;
 
 import com.google.gson.JsonArray;
 import com.google.gson.JsonObject;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 /**
- * Presigned V4 post policy.
+ * Presigned V4 post policy. Instances of {@code PostPolicyV4} include a URL and a map of fields
+ * that can be specified in an HTML form to submit a POST request to upload an object.
  *
- * @see <a href="https://cloud.google.com/storage/docs/xml-api/post-object">POST Object</a>
+ * <p>See <a href="https://cloud.google.com/storage/docs/xml-api/post-object">POST Object</a> for
+ * details of upload by using HTML forms.
+ *
+ * <p>See {@link Storage#generateSignedPostPolicyV4(BlobInfo, long, TimeUnit,
+ * PostPolicyV4.PostFieldsV4, PostPolicyV4.PostConditionsV4, Storage.PostPolicyV4Option...)} for
+ * example of usage.
  */
 public final class PostPolicyV4 {
   private final String url;
   private final Map<String, String> fields;
 
   private PostPolicyV4(String url, Map<String, String> fields) {
+    try {
+      if (!new URI(url).isAbsolute()) {
+        throw new IllegalArgumentException(url + " is not an absolute URL");
+      }
+    } catch (URISyntaxException e) {
+      throw new IllegalArgumentException(e);
+    }
+    PostFieldsV4.validateFields(fields);
+
     this.url = url;
-    this.fields = fields;
+    this.fields = Collections.unmodifiableMap(fields);
   }
 
+  /**
+   * Constructs {@code PostPolicyV4} instance of the given URL and fields map.
+   *
+   * @param url URL for the HTTP POST request
+   * @param fields HTML form fields
+   * @return constructed object
+   * @throws IllegalArgumentException if URL is malformed or fields are not valid
+   */
   public static PostPolicyV4 of(String url, Map<String, String> fields) {
     return new PostPolicyV4(url, fields);
   }
 
+  /** Returns the URL for the HTTP POST request */
   public String getUrl() {
     return url;
   }
 
+  /** Returns the HTML form fields */
   public Map<String, String> getFields() {
     return fields;
   }
 
   /**
-   * Class representing which fields to specify in a V4 POST request.
+   * A helper class to define fields to be specified in a V4 POST request. Instance of this class
+   * helps to construct {@code PostPolicyV4} objects. Used in: {@link
+   * Storage#generateSignedPostPolicyV4(BlobInfo, long, TimeUnit, PostPolicyV4.PostFieldsV4,
+   * PostPolicyV4.PostConditionsV4, Storage.PostPolicyV4Option...)}.
    *
    * @see <a href="https://cloud.google.com/storage/docs/xml-api/post-object#form_fields">POST
    *     Object Form fields</a>
    */
   public static final class PostFieldsV4 {
     private final Map<String, String> fieldsMap;
+    private static final List<String> VALID_FIELDS =
+        Arrays.asList(
+            "acl",
+            "bucket",
+            "cache-control",
+            "content-disposition",
+            "content-encoding",
+            "content-type",
+            "expires",
+            "file",
+            "key",
+            "policy",
+            "success_action_redirect",
+            "success_action_status",
+            "x-goog-algorithm",
+            "x-goog-credential",
+            "x-goog-date",
+            "x-goog-signature");
+
+    private static void validateFields(Map<String, String> fields) {
+      for (String key : fields.keySet()) {
+        if (!VALID_FIELDS.contains(key.toLowerCase())
+            && !key.startsWith(Builder.CUSTOM_FIELD_PREFIX)) {
+          throw new IllegalArgumentException("Invalid key: " + key);
+        }
+      }
+    }
 
     private PostFieldsV4(Builder builder) {
-      this.fieldsMap = builder.fieldsMap;
+      this(builder.fieldsMap);
     }
 
     private PostFieldsV4(Map<String, String> fields) {
-      this.fieldsMap = fields;
+      validateFields(fields);
+      this.fieldsMap = Collections.unmodifiableMap(fields);
     }
 
+    /**
+     * Constructs {@code PostPolicyV4.PostFieldsV4} object of the given field map.
+     *
+     * @param fields a map of the HTML form fields
+     * @return constructed object
+     * @throws IllegalArgumentException if an unsupported field is specified
+     */
     public static PostFieldsV4 of(Map<String, String> fields) {
       return new PostFieldsV4(fields);
     }
@@ -112,8 +181,14 @@ public final class PostPolicyV4 {
         return this;
       }
 
+      /**
+       * @deprecated Invocation of this method has no effect, because all valid HTML form fields
+       *     except Content-Length can use exact matching. Use {@link
+       *     PostPolicyV4.PostConditionsV4.Builder#addContentLengthRangeCondition(int, int)} to
+       *     specify a range for the content-length.
+       */
+      @Deprecated
       public Builder setContentLength(int contentLength) {
-        fieldsMap.put("content-length", "" + contentLength);
         return this;
       }
 
@@ -122,7 +197,7 @@ public final class PostPolicyV4 {
         return this;
       }
 
-      /** @deprecated use {@link #setExpires(String)} */
+      /** @deprecated Use {@link #setExpires(String)}. */
       @Deprecated
       public Builder Expires(String expires) {
         return setExpires(expires);
@@ -143,7 +218,7 @@ public final class PostPolicyV4 {
         return this;
       }
 
-      /** @deprecated use {@link #setCustomMetadataField(String, String)} */
+      /** @deprecated Use {@link #setCustomMetadataField(String, String)}. */
       @Deprecated
       public Builder AddCustomMetadataField(String field, String value) {
         return setCustomMetadataField(field, value);
@@ -151,7 +226,7 @@ public final class PostPolicyV4 {
 
       public Builder setCustomMetadataField(String field, String value) {
         if (!field.startsWith(CUSTOM_FIELD_PREFIX)) {
-          field = CUSTOM_FIELD_PREFIX + value;
+          field = CUSTOM_FIELD_PREFIX + field;
         }
         fieldsMap.put(field, value);
         return this;
@@ -160,7 +235,9 @@ public final class PostPolicyV4 {
   }
 
   /**
-   * Class for specifying conditions in a V4 POST Policy document.
+   * A helper class for specifying conditions in a V4 POST Policy document. Used in: {@link
+   * Storage#generateSignedPostPolicyV4(BlobInfo, long, TimeUnit, PostPolicyV4.PostFieldsV4,
+   * PostPolicyV4.PostConditionsV4, Storage.PostPolicyV4Option...)}.
    *
    * @see <a href="https://cloud.google.com/storage/docs/authentication/signatures#policy-document">
    *     Policy document</a>
@@ -183,14 +260,14 @@ public final class PostPolicyV4 {
     }
 
     public Set<ConditionV4> getConditions() {
-      return conditions;
+      return Collections.unmodifiableSet(conditions);
     }
 
     public static class Builder {
-      Set<ConditionV4> conditions;
+      private final Set<ConditionV4> conditions;
 
       private Builder() {
-        this.conditions = new LinkedHashSet<>();
+        this(new LinkedHashSet<ConditionV4>());
       }
 
       private Builder(Set<ConditionV4> conditions) {
@@ -206,64 +283,93 @@ public final class PostPolicyV4 {
       }
 
       public Builder addAclCondition(ConditionV4Type type, String acl) {
+        checkType(type, "acl");
         conditions.add(new ConditionV4(type, "acl", acl));
         return this;
       }
 
       public Builder addBucketCondition(ConditionV4Type type, String bucket) {
+        checkType(type, "bucket");
         conditions.add(new ConditionV4(type, "bucket", bucket));
         return this;
       }
 
       public Builder addCacheControlCondition(ConditionV4Type type, String cacheControl) {
+        checkType(type, "cache-control");
         conditions.add(new ConditionV4(type, "cache-control", cacheControl));
         return this;
       }
 
       public Builder addContentDispositionCondition(
           ConditionV4Type type, String contentDisposition) {
+        checkType(type, "content-disposition");
         conditions.add(new ConditionV4(type, "content-disposition", contentDisposition));
         return this;
       }
 
       public Builder addContentEncodingCondition(ConditionV4Type type, String contentEncoding) {
+        checkType(type, "content-encoding");
         conditions.add(new ConditionV4(type, "content-encoding", contentEncoding));
         return this;
       }
 
+      /**
+       * @deprecated Invocation of this method has no effect. Use {@link
+       *     #addContentLengthRangeCondition(int, int)} to specify a range for the content-length.
+       */
       public Builder addContentLengthCondition(ConditionV4Type type, int contentLength) {
-        conditions.add(new ConditionV4(type, "content-length", "" + contentLength));
         return this;
       }
 
       public Builder addContentTypeCondition(ConditionV4Type type, String contentType) {
+        checkType(type, "content-type");
         conditions.add(new ConditionV4(type, "content-type", contentType));
         return this;
       }
 
+      /** @deprecated Use {@link #addExpiresCondition(long)} */
+      @Deprecated
       public Builder addExpiresCondition(ConditionV4Type type, long expires) {
-        conditions.add(new ConditionV4(type, "expires", dateFormat.format(expires)));
-        return this;
+        return addExpiresCondition(expires);
       }
 
+      /** @deprecated Use {@link #addExpiresCondition(String)} */
+      @Deprecated
       public Builder addExpiresCondition(ConditionV4Type type, String expires) {
-        conditions.add(new ConditionV4(type, "expires", expires));
+        return addExpiresCondition(expires);
+      }
+
+      public Builder addExpiresCondition(long expires) {
+        return addExpiresCondition(dateFormat.format(expires));
+      }
+
+      public Builder addExpiresCondition(String expires) {
+        conditions.add(new ConditionV4(ConditionV4Type.MATCHES, "expires", expires));
         return this;
       }
 
       public Builder addKeyCondition(ConditionV4Type type, String key) {
+        checkType(type, "key");
         conditions.add(new ConditionV4(type, "key", key));
         return this;
       }
 
       public Builder addSuccessActionRedirectUrlCondition(
           ConditionV4Type type, String successActionRedirectUrl) {
+        checkType(type, "success_action_redirect");
         conditions.add(new ConditionV4(type, "success_action_redirect", successActionRedirectUrl));
         return this;
       }
 
+      /** @deprecated Use {@link #addSuccessActionStatusCondition(int)} */
+      @Deprecated
       public Builder addSuccessActionStatusCondition(ConditionV4Type type, int status) {
-        conditions.add(new ConditionV4(type, "success_action_status", "" + status));
+        return addSuccessActionStatusCondition(status);
+      }
+
+      public Builder addSuccessActionStatusCondition(int status) {
+        conditions.add(
+            new ConditionV4(ConditionV4Type.MATCHES, "success_action_status", "" + status));
         return this;
       }
 
@@ -276,11 +382,17 @@ public final class PostPolicyV4 {
         conditions.add(new ConditionV4(type, field, value));
         return this;
       }
+
+      private void checkType(ConditionV4Type type, String field) {
+        if (type != ConditionV4Type.MATCHES && type != ConditionV4Type.STARTS_WITH) {
+          throw new IllegalArgumentException("Field " + field + " can't use " + type);
+        }
+      }
     }
   }
 
   /**
-   * Class for a V4 POST Policy document.
+   * Class for a V4 POST Policy document. Used by Storage to construct {@code PostPolicyV4} objects.
    *
    * @see <a href="https://cloud.google.com/storage/docs/authentication/signatures#policy-document">
    *     Policy document</a>
@@ -367,9 +479,20 @@ public final class PostPolicyV4 {
   }
 
   public enum ConditionV4Type {
-    MATCHES,
-    STARTS_WITH,
-    CONTENT_LENGTH_RANGE
+    MATCHES("eq"),
+    STARTS_WITH("starts-with"),
+    CONTENT_LENGTH_RANGE("content-length-range");
+
+    private final String name;
+
+    ConditionV4Type(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
   }
 
   /**
@@ -378,12 +501,12 @@ public final class PostPolicyV4 {
    * @see <a href="https://cloud.google.com/storage/docs/authentication/signatures#policy-document">
    *     Policy document</a>
    */
-  static final class ConditionV4 {
-    final ConditionV4Type type;
-    final String operand1;
-    final String operand2;
+  public static final class ConditionV4 {
+    public final ConditionV4Type type;
+    public final String operand1;
+    public final String operand2;
 
-    private ConditionV4(ConditionV4Type type, String operand1, String operand2) {
+    ConditionV4(ConditionV4Type type, String operand1, String operand2) {
       this.type = type;
       this.operand1 = operand1;
       this.operand2 = operand2;
@@ -401,5 +524,18 @@ public final class PostPolicyV4 {
     public int hashCode() {
       return Objects.hash(type, operand1, operand2);
     }
+
+    /**
+     * Examples of returned strings: {@code ["eq", "$key", "test-object"]}, {@code ["starts-with",
+     * "$acl", "public"]}, {@code ["content-length-range", 246, 266]}.
+     */
+    @Override
+    public String toString() {
+      String body =
+          type == ConditionV4Type.CONTENT_LENGTH_RANGE
+              ? operand1 + ", " + operand2
+              : "\"$" + operand1 + "\", \"" + operand2 + "\"";
+      return "[\"" + type + "\", " + body + "]";
+    }
   }
 }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java
index 82ba1750..6b8fc499 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java
@@ -2843,6 +2843,8 @@ public interface Storage extends Service<StorageOptions> {
    * }</pre>
    *
    * @param blobInfo the blob uploaded in the form
+   * @param duration time before expiration
+   * @param unit duration time unit
    * @param fields the fields specified in the form
    * @param conditions which conditions every upload must satisfy
    * @param duration how long until the form expires, in milliseconds
@@ -2861,9 +2863,8 @@ public interface Storage extends Service<StorageOptions> {
 
   /**
    * Generates a presigned post policy without any conditions. Automatically creates required
-   * conditions. See full documentation for generateSignedPostPolicyV4( BlobInfo blobInfo, long
-   * duration, TimeUnit unit, PostFieldsV4 fields, PostConditionsV4 conditions,
-   * PostPolicyV4Option... options) above.
+   * conditions. See full documentation for {@link #generateSignedPostPolicyV4(BlobInfo, long,
+   * TimeUnit, PostPolicyV4.PostFieldsV4, PostPolicyV4.PostConditionsV4, PostPolicyV4Option...)}.
    */
   PostPolicyV4 generateSignedPostPolicyV4(
       BlobInfo blobInfo,
@@ -2874,9 +2875,8 @@ public interface Storage extends Service<StorageOptions> {
 
   /**
    * Generates a presigned post policy without any fields. Automatically creates required fields.
-   * See full documentation for generateSignedPostPolicyV4( BlobInfo blobInfo, long duration,
-   * TimeUnit unit, PostFieldsV4 fields, PostConditionsV4 conditions, PostPolicyV4Option... options)
-   * above.
+   * See full documentation for {@link #generateSignedPostPolicyV4(BlobInfo, long, TimeUnit,
+   * PostPolicyV4.PostFieldsV4, PostPolicyV4.PostConditionsV4, PostPolicyV4Option...)}.
    */
   PostPolicyV4 generateSignedPostPolicyV4(
       BlobInfo blobInfo,
@@ -2887,9 +2887,9 @@ public interface Storage extends Service<StorageOptions> {
 
   /**
    * Generates a presigned post policy without any fields or conditions. Automatically creates
-   * required fields and conditions. See full documentation for generateSignedPostPolicyV4( BlobInfo
-   * blobInfo, long duration, TimeUnit unit, PostFieldsV4 fields, PostConditionsV4 conditions,
-   * PostPolicyV4Option... options) above.
+   * required fields and conditions. See full documentation for {@link
+   * #generateSignedPostPolicyV4(BlobInfo, long, TimeUnit, PostPolicyV4.PostFieldsV4,
+   * PostPolicyV4.PostConditionsV4, PostPolicyV4Option...)}.
    */
   PostPolicyV4 generateSignedPostPolicyV4(
       BlobInfo blobInfo, long duration, TimeUnit unit, PostPolicyV4Option... options);
diff --git a/pom.xml b/pom.xml
index ba836071..3672201d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -70,7 +70,7 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-shared-dependencies</artifactId>
-        <version>0.8.4</version>
+        <version>0.8.6</version>
         <type>pom</type>
         <scope>import</scope>
       </dependency>
@@ -78,7 +78,7 @@
       <dependency>
         <groupId>com.google.apis</groupId>
         <artifactId>google-api-services-storage</artifactId>
-        <version>v1-rev20200611-1.30.10</version>
+        <version>v1-rev20200727-1.30.10</version>
       </dependency>
       <dependency>
         <groupId>org.easymock</groupId>
diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml
index e1a72331..a2e44bb9 100644
--- a/samples/snippets/pom.xml
+++ b/samples/snippets/pom.xml
@@ -30,7 +30,7 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>libraries-bom</artifactId>
-        <version>8.1.0</version>
+        <version>9.1.0</version>
         <type>pom</type>
         <scope>import</scope>
       </dependency>
diff --git a/synth.metadata b/synth.metadata
index f19c30e8..c283453c 100644
--- a/synth.metadata
+++ b/synth.metadata
@@ -4,14 +4,14 @@
       "git": {
         "name": ".",
         "remote": "https://github.com/googleapis/java-storage.git",
-        "sha": "f8a4b12517c661881d7b7c65f796c1c8f1cf3ae9"
+        "sha": "8602b81eae95868e184fd4ab290396707bd21a8e"
       }
     },
     {
       "git": {
         "name": "synthtool",
         "remote": "https://github.com/googleapis/synthtool.git",
-        "sha": "4f2c9f752a94042472fc03c5bd9e06e89817d2bd"
+        "sha": "968465a1cad496e1292ef4584a054a35f756ff94"
       }
     }
   ],
@@ -24,33 +24,23 @@
     ".github/release-please.yml",
     ".github/trusted-contribution.yml",
     ".github/workflows/ci.yaml",
+    ".github/workflows/samples.yaml",
     ".kokoro/build.bat",
     ".kokoro/build.sh",
     ".kokoro/coerce_logs.sh",
     ".kokoro/common.cfg",
     ".kokoro/common.sh",
     ".kokoro/continuous/common.cfg",
-    ".kokoro/continuous/dependencies.cfg",
-    ".kokoro/continuous/integration.cfg",
-    ".kokoro/continuous/java11.cfg",
-    ".kokoro/continuous/java7.cfg",
-    ".kokoro/continuous/java8-osx.cfg",
-    ".kokoro/continuous/java8-win.cfg",
     ".kokoro/continuous/java8.cfg",
-    ".kokoro/continuous/lint.cfg",
-    ".kokoro/continuous/propose_release.cfg",
-    ".kokoro/continuous/samples.cfg",
     ".kokoro/dependencies.sh",
     ".kokoro/linkage-monitor.sh",
     ".kokoro/nightly/common.cfg",
-    ".kokoro/nightly/dependencies.cfg",
     ".kokoro/nightly/integration.cfg",
     ".kokoro/nightly/java11.cfg",
     ".kokoro/nightly/java7.cfg",
     ".kokoro/nightly/java8-osx.cfg",
     ".kokoro/nightly/java8-win.cfg",
     ".kokoro/nightly/java8.cfg",
-    ".kokoro/nightly/lint.cfg",
     ".kokoro/nightly/samples.cfg",
     ".kokoro/populate-secrets.sh",
     ".kokoro/presubmit/clirr.cfg",
@@ -79,7 +69,6 @@
     ".kokoro/release/stage.sh",
     ".kokoro/trampoline.sh",
     "CODE_OF_CONDUCT.md",
-    "CONTRIBUTING.md",
     "LICENSE",
     "README.md",
     "codecov.yaml",


Test patch:
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobInfoTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobInfoTest.java
index ea4f6dbd..d4119971 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobInfoTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobInfoTest.java
@@ -67,6 +67,7 @@ public class BlobInfoTest {
   private static final Long SIZE = 1024L;
   private static final Long UPDATE_TIME = DELETE_TIME - 1L;
   private static final Long CREATE_TIME = UPDATE_TIME - 1L;
+  private static final Long CUSTOM_TIME = CREATE_TIME - 1L;
   private static final String ENCRYPTION_ALGORITHM = "AES256";
   private static final String KEY_SHA256 = "keySha";
   private static final CustomerEncryption CUSTOMER_ENCRYPTION =
@@ -101,6 +102,7 @@ public class BlobInfoTest {
           .setSize(SIZE)
           .setUpdateTime(UPDATE_TIME)
           .setCreateTime(CREATE_TIME)
+          .setCustomTime(CUSTOM_TIME)
           .setStorageClass(STORAGE_CLASS)
           .setKmsKeyName(KMS_KEY_NAME)
           .setEventBasedHold(EVENT_BASED_HOLD)
@@ -195,6 +197,7 @@ public class BlobInfoTest {
     assertEquals(SIZE, BLOB_INFO.getSize());
     assertEquals(UPDATE_TIME, BLOB_INFO.getUpdateTime());
     assertEquals(CREATE_TIME, BLOB_INFO.getCreateTime());
+    assertEquals(CUSTOM_TIME, BLOB_INFO.getCustomTime());
     assertEquals(STORAGE_CLASS, BLOB_INFO.getStorageClass());
     assertEquals(KMS_KEY_NAME, BLOB_INFO.getKmsKeyName());
     assertEquals(EVENT_BASED_HOLD, BLOB_INFO.getEventBasedHold());
@@ -214,6 +217,7 @@ public class BlobInfoTest {
     assertNull(DIRECTORY_INFO.getCrc32c());
     assertNull(DIRECTORY_INFO.getCrc32cToHexString());
     assertNull(DIRECTORY_INFO.getCreateTime());
+    assertNull(DIRECTORY_INFO.getCustomTime());
     assertNull(DIRECTORY_INFO.getDeleteTime());
     assertNull(DIRECTORY_INFO.getEtag());
     assertNull(DIRECTORY_INFO.getGeneration());
@@ -257,6 +261,7 @@ public class BlobInfoTest {
     assertEquals(expected.getOwner(), value.getOwner());
     assertEquals(expected.getSelfLink(), value.getSelfLink());
     assertEquals(expected.getSize(), value.getSize());
+    assertEquals(expected.getCustomTime(), value.getCustomTime());
     assertEquals(expected.getUpdateTime(), value.getUpdateTime());
     assertEquals(expected.getStorageClass(), value.getStorageClass());
     assertEquals(expected.getKmsKeyName(), value.getKmsKeyName());
@@ -299,6 +304,7 @@ public class BlobInfoTest {
     assertNull(blobInfo.getCrc32c());
     assertNull(blobInfo.getCrc32cToHexString());
     assertNull(blobInfo.getCreateTime());
+    assertNull(blobInfo.getCustomTime());
     assertNull(blobInfo.getDeleteTime());
     assertNull(blobInfo.getEtag());
     assertNull(blobInfo.getGeneration());
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobTest.java
index 7ff384fb..2ff21b71 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobTest.java
@@ -91,6 +91,7 @@ public class BlobTest {
   private static final Long SIZE = 1024L;
   private static final Long UPDATE_TIME = DELETE_TIME - 1L;
   private static final Long CREATE_TIME = UPDATE_TIME - 1L;
+  private static final Long CUSTOM_TIME = CREATE_TIME - 1L;
   private static final String ENCRYPTION_ALGORITHM = "AES256";
   private static final String KEY_SHA256 = "keySha";
   private static final BlobInfo.CustomerEncryption CUSTOMER_ENCRYPTION =
@@ -122,6 +123,7 @@ public class BlobTest {
           .setSize(SIZE)
           .setUpdateTime(UPDATE_TIME)
           .setCreateTime(CREATE_TIME)
+          .setCustomTime(CUSTOM_TIME)
           .setCustomerEncryption(CUSTOMER_ENCRYPTION)
           .setKmsKeyName(KMS_KEY_NAME)
           .setEventBasedHold(EVENT_BASED_HOLD)
@@ -510,6 +512,7 @@ public class BlobTest {
             .setContentLanguage(CONTENT_LANGUAGE)
             .setCrc32c(CRC32)
             .setCreateTime(CREATE_TIME)
+            .setCustomTime(CUSTOM_TIME)
             .setCustomerEncryption(CUSTOMER_ENCRYPTION)
             .setKmsKeyName(KMS_KEY_NAME)
             .setEventBasedHold(EVENT_BASED_HOLD)
@@ -539,6 +542,7 @@ public class BlobTest {
     assertEquals(CRC32, blob.getCrc32c());
     assertEquals(CRC32_HEX_STRING, blob.getCrc32cToHexString());
     assertEquals(CREATE_TIME, blob.getCreateTime());
+    assertEquals(CUSTOM_TIME, blob.getCustomTime());
     assertEquals(CUSTOMER_ENCRYPTION, blob.getCustomerEncryption());
     assertEquals(KMS_KEY_NAME, blob.getKmsKeyName());
     assertEquals(EVENT_BASED_HOLD, blob.getEventBasedHold());
@@ -589,6 +593,7 @@ public class BlobTest {
     assertNull(blob.getSelfLink());
     assertEquals(0L, (long) blob.getSize());
     assertNull(blob.getUpdateTime());
+    assertNull(blob.getCustomTime());
     assertTrue(blob.isDirectory());
   }
 
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/BucketInfoTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/BucketInfoTest.java
index 425cfb70..8def7c30 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/BucketInfoTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/BucketInfoTest.java
@@ -21,6 +21,7 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
+import com.google.api.client.util.DateTime;
 import com.google.api.services.storage.model.Bucket;
 import com.google.api.services.storage.model.Bucket.Lifecycle.Rule;
 import com.google.cloud.storage.Acl.Project;
@@ -328,6 +329,27 @@ public class BucketInfoTest {
         setStorageClassLifecycleRule.getAction().getStorageClass());
     assertTrue(setStorageClassLifecycleRule.getCondition().getIsLive());
     assertEquals(10, setStorageClassLifecycleRule.getCondition().getNumNewerVersions().intValue());
+
+    Rule lifecycleRule =
+        new LifecycleRule(
+                LifecycleAction.newSetStorageClassAction(StorageClass.COLDLINE),
+                LifecycleCondition.newBuilder()
+                    .setIsLive(true)
+                    .setNumberOfNewerVersions(10)
+                    .setDaysSinceNoncurrentTime(30)
+                    .setNoncurrentTimeBefore(new DateTime(System.currentTimeMillis()))
+                    .setCustomTimeBefore(new DateTime(System.currentTimeMillis()))
+                    .setDaysSinceCustomTime(30)
+                    .build())
+            .toPb();
+    assertEquals(StorageClass.COLDLINE.toString(), lifecycleRule.getAction().getStorageClass());
+    assertTrue(lifecycleRule.getCondition().getIsLive());
+    assertEquals(10, lifecycleRule.getCondition().getNumNewerVersions().intValue());
+    assertEquals(30, lifecycleRule.getCondition().getDaysSinceNoncurrentTime().intValue());
+    assertNotNull(lifecycleRule.getCondition().getNoncurrentTimeBefore());
+    assertEquals(StorageClass.COLDLINE.toString(), lifecycleRule.getAction().getStorageClass());
+    assertEquals(30, lifecycleRule.getCondition().getDaysSinceCustomTime().intValue());
+    assertNotNull(lifecycleRule.getCondition().getCustomTimeBefore());
   }
 
   @Test
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/PostPolicyV4Test.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/PostPolicyV4Test.java
new file mode 100644
index 00000000..8213f609
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/PostPolicyV4Test.java
@@ -0,0 +1,592 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.storage;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.text.SimpleDateFormat;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import org.junit.Test;
+
+public class PostPolicyV4Test {
+  private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
+
+  private void assertNotSameButEqual(Map<String, String> expected, Map<String, String> returned) {
+    assertNotSame(expected, returned);
+    assertEquals("map sizes", expected.size(), returned.size());
+    for (String key : expected.keySet()) {
+      assertEquals("value of $" + key, expected.get(key), returned.get(key));
+    }
+  }
+
+  private static final String[] VALID_FIELDS = {
+    "acl",
+    "bucket",
+    "cache-control",
+    "content-disposition",
+    "content-encoding",
+    "content-type",
+    "expires",
+    "file",
+    "key",
+    "policy",
+    "success_action_redirect",
+    "success_action_status",
+    "x-goog-algorithm",
+    "x-goog-credential",
+    "x-goog-date",
+    "x-goog-signature",
+  };
+
+  private static final String CUSTOM_PREFIX = "x-goog-meta-";
+
+  private static Map<String, String> initAllFields() {
+    Map<String, String> fields = new HashMap<>();
+    for (String key : VALID_FIELDS) {
+      fields.put(key, "value of " + key);
+    }
+    fields.put(CUSTOM_PREFIX + "custom", "value of custom field");
+    return Collections.unmodifiableMap(fields);
+  }
+
+  private static final Map<String, String> ALL_FIELDS = initAllFields();
+
+  @Test
+  public void testPostPolicyV4_of() {
+    String url = "http://example.com";
+    PostPolicyV4 policy = PostPolicyV4.of(url, ALL_FIELDS);
+    assertEquals(url, policy.getUrl());
+    assertNotSameButEqual(ALL_FIELDS, policy.getFields());
+  }
+
+  @Test
+  public void testPostPolicyV4_ofMalformedURL() {
+    try {
+      PostPolicyV4.of("example.com", new HashMap<String, String>());
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertEquals("example.com is not an absolute URL", e.getMessage());
+    }
+
+    try {
+      PostPolicyV4.of("Scio nescio", new HashMap<String, String>());
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertEquals(
+          "java.net.URISyntaxException: Illegal character in path at index 4: Scio nescio",
+          e.getMessage());
+    }
+  }
+
+  @Test
+  public void testPostPolicyV4_ofInvalidField() {
+    Map<String, String> fields = new HashMap<>(ALL_FIELDS);
+    fields.put("$file", "file.txt");
+    try {
+      PostPolicyV4.of("http://google.com", fields);
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertEquals("Invalid key: $file", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testPostFieldsV4_of() {
+    PostPolicyV4.PostFieldsV4 fields = PostPolicyV4.PostFieldsV4.of(ALL_FIELDS);
+    assertNotSameButEqual(ALL_FIELDS, fields.getFieldsMap());
+  }
+
+  @Test
+  public void testPostFieldsV4_ofInvalidField() {
+    Map<String, String> map = new HashMap<>();
+    map.put("$file", "file.txt");
+    try {
+      PostPolicyV4.PostFieldsV4.of(map);
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertEquals("Invalid key: $file", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testPostPolicyV4_builder() {
+    PostPolicyV4.PostFieldsV4.Builder builder = PostPolicyV4.PostFieldsV4.newBuilder();
+    builder.setAcl("acl");
+    builder.setCacheControl("cache-control");
+    builder.setContentDisposition("content-disposition");
+    builder.setContentType("content-type");
+    builder.setExpires("expires");
+    builder.setSuccessActionRedirect("success_action_redirect");
+    Map<String, String> map = builder.build().getFieldsMap();
+    assertEquals("map size", 6, map.size());
+    for (String key : map.keySet()) {
+      assertEquals("value of $" + key, key, map.get(key));
+    }
+
+    Map<String, String> expectedUpdated = new HashMap<>(map);
+    builder.setCustomMetadataField("xxx", "XXX");
+    builder.setCustomMetadataField(CUSTOM_PREFIX + "yyy", "YYY");
+    builder.setAcl(null);
+    builder.setContentType("new-content-type");
+    builder.setSuccessActionStatus(42);
+    expectedUpdated.put(CUSTOM_PREFIX + "xxx", "XXX");
+    expectedUpdated.put(CUSTOM_PREFIX + "yyy", "YYY");
+    expectedUpdated.put("acl", null);
+    expectedUpdated.put("content-type", "new-content-type");
+    expectedUpdated.put("success_action_status", "42");
+    Map<String, String> updated = builder.build().getFieldsMap();
+    assertNotSameButEqual(expectedUpdated, updated);
+  }
+
+  @Test
+  public void testPostPolicyV4_setContentLength() {
+    PostPolicyV4.PostFieldsV4.Builder builder = PostPolicyV4.PostFieldsV4.newBuilder();
+    builder.setContentLength(12345);
+    assertTrue(builder.build().getFieldsMap().isEmpty());
+  }
+
+  @Test
+  public void testPostConditionsV4_builder() {
+    PostPolicyV4.PostConditionsV4.Builder builder = PostPolicyV4.PostConditionsV4.newBuilder();
+    assertTrue(builder.build().getConditions().isEmpty());
+
+    builder.addAclCondition(PostPolicyV4.ConditionV4Type.STARTS_WITH, "public");
+    builder.addBucketCondition(PostPolicyV4.ConditionV4Type.MATCHES, "travel-maps");
+    builder.addContentLengthRangeCondition(0, 100000);
+
+    PostPolicyV4.PostConditionsV4 postConditionsV4 = builder.build();
+    Set<PostPolicyV4.ConditionV4> conditions = postConditionsV4.getConditions();
+    assertEquals(3, conditions.size());
+
+    try {
+      conditions.clear();
+      fail();
+    } catch (UnsupportedOperationException e) {
+      // expected
+    }
+
+    PostPolicyV4.PostConditionsV4 postConditionsV4Extended =
+        postConditionsV4
+            .toBuilder()
+            .addCustomCondition(PostPolicyV4.ConditionV4Type.STARTS_WITH, "key", "")
+            .build();
+    assertEquals(4, postConditionsV4Extended.getConditions().size());
+  }
+
+  interface ConditionTest {
+    /**
+     * Calls one of addCondition method on the given builder and returns expected ConditionV4
+     * object.
+     */
+    PostPolicyV4.ConditionV4 addCondition(PostPolicyV4.PostConditionsV4.Builder builder);
+  }
+
+  @Test
+  public void testPostConditionsV4_addCondition() {
+    // shortcuts
+    final PostPolicyV4.ConditionV4Type eq = PostPolicyV4.ConditionV4Type.MATCHES;
+    final PostPolicyV4.ConditionV4Type startsWith = PostPolicyV4.ConditionV4Type.STARTS_WITH;
+    final PostPolicyV4.ConditionV4Type range = PostPolicyV4.ConditionV4Type.CONTENT_LENGTH_RANGE;
+
+    ConditionTest[] cases = {
+      new ConditionTest() {
+        @Override
+        public PostPolicyV4.ConditionV4 addCondition(
+            PostPolicyV4.PostConditionsV4.Builder builder) {
+          builder.addContentLengthRangeCondition(123, 456);
+          return new PostPolicyV4.ConditionV4(range, "123", "456");
+        }
+
+        @Override
+        public String toString() {
+          return "addContentLengthRangeCondition()";
+        }
+      },
+      new ConditionTest() {
+        @Override
+        public PostPolicyV4.ConditionV4 addCondition(
+            PostPolicyV4.PostConditionsV4.Builder builder) {
+          long date = 2000000000000L;
+          builder.addExpiresCondition(date);
+          return new PostPolicyV4.ConditionV4(eq, "expires", dateFormat.format(date));
+        }
+
+        @Override
+        public String toString() {
+          return "addExpiresCondition(long)";
+        }
+      },
+      new ConditionTest() {
+        @Override
+        public PostPolicyV4.ConditionV4 addCondition(
+            PostPolicyV4.PostConditionsV4.Builder builder) {
+          builder.addExpiresCondition("2030-Dec-31");
+          return new PostPolicyV4.ConditionV4(eq, "expires", "2030-Dec-31");
+        }
+
+        @Override
+        public String toString() {
+          return "addExpiresCondition(String)";
+        }
+      },
+      new ConditionTest() {
+        @Override
+        public PostPolicyV4.ConditionV4 addCondition(
+            PostPolicyV4.PostConditionsV4.Builder builder) {
+          builder.addExpiresCondition(range, 0);
+          return new PostPolicyV4.ConditionV4(eq, "expires", dateFormat.format(0));
+        }
+
+        @Override
+        public String toString() {
+          return "@deprecated addExpiresCondition(type,long)";
+        }
+      },
+      new ConditionTest() {
+        @Override
+        public PostPolicyV4.ConditionV4 addCondition(
+            PostPolicyV4.PostConditionsV4.Builder builder) {
+          builder.addExpiresCondition(startsWith, "2030-Dec-31");
+          return new PostPolicyV4.ConditionV4(eq, "expires", "2030-Dec-31");
+        }
+
+        @Override
+        public String toString() {
+          return "@deprecated addExpiresCondition(type,String)";
+        }
+      },
+      new ConditionTest() {
+        @Override
+        public PostPolicyV4.ConditionV4 addCondition(
+            PostPolicyV4.PostConditionsV4.Builder builder) {
+          builder.addSuccessActionStatusCondition(202);
+          return new PostPolicyV4.ConditionV4(eq, "success_action_status", "202");
+        }
+
+        @Override
+        public String toString() {
+          return "addSuccessActionStatusCondition(int)";
+        }
+      },
+      new ConditionTest() {
+        @Override
+        public PostPolicyV4.ConditionV4 addCondition(
+            PostPolicyV4.PostConditionsV4.Builder builder) {
+          builder.addSuccessActionStatusCondition(startsWith, 202);
+          return new PostPolicyV4.ConditionV4(eq, "success_action_status", "202");
+        }
+
+        @Override
+        public String toString() {
+          return "@deprecated addSuccessActionStatusCondition(type,int)";
+        }
+      },
+      new ConditionTest() {
+        @Override
+        public PostPolicyV4.ConditionV4 addCondition(
+            PostPolicyV4.PostConditionsV4.Builder builder) {
+          builder.addAclCondition(startsWith, "read");
+          return new PostPolicyV4.ConditionV4(startsWith, "acl", "read");
+        }
+
+        @Override
+        public String toString() {
+          return "addAclCondition()";
+        }
+      },
+      new ConditionTest() {
+        @Override
+        public PostPolicyV4.ConditionV4 addCondition(
+            PostPolicyV4.PostConditionsV4.Builder builder) {
+          builder.addBucketCondition(eq, "my-bucket");
+          return new PostPolicyV4.ConditionV4(eq, "bucket", "my-bucket");
+        }
+
+        @Override
+        public String toString() {
+          return "addBucketCondition()";
+        }
+      },
+      new ConditionTest() {
+        @Override
+        public PostPolicyV4.ConditionV4 addCondition(
+            PostPolicyV4.PostConditionsV4.Builder builder) {
+          builder.addCacheControlCondition(eq, "false");
+          return new PostPolicyV4.ConditionV4(eq, "cache-control", "false");
+        }
+
+        @Override
+        public String toString() {
+          return "addCacheControlCondition()";
+        }
+      },
+      new ConditionTest() {
+        @Override
+        public PostPolicyV4.ConditionV4 addCondition(
+            PostPolicyV4.PostConditionsV4.Builder builder) {
+          builder.addContentDispositionCondition(startsWith, "gzip");
+          return new PostPolicyV4.ConditionV4(startsWith, "content-disposition", "gzip");
+        }
+
+        @Override
+        public String toString() {
+          return "addContentDispositionCondition()";
+        }
+      },
+      new ConditionTest() {
+        @Override
+        public PostPolicyV4.ConditionV4 addCondition(
+            PostPolicyV4.PostConditionsV4.Builder builder) {
+          builder.addContentEncodingCondition(eq, "koi8");
+          return new PostPolicyV4.ConditionV4(eq, "content-encoding", "koi8");
+        }
+
+        @Override
+        public String toString() {
+          return "addContentEncodingCondition()";
+        }
+      },
+      new ConditionTest() {
+        @Override
+        public PostPolicyV4.ConditionV4 addCondition(
+            PostPolicyV4.PostConditionsV4.Builder builder) {
+          builder.addContentTypeCondition(startsWith, "application/");
+          return new PostPolicyV4.ConditionV4(startsWith, "content-type", "application/");
+        }
+
+        @Override
+        public String toString() {
+          return "addContentTypeCondition()";
+        }
+      },
+      new ConditionTest() {
+        @Override
+        public PostPolicyV4.ConditionV4 addCondition(
+            PostPolicyV4.PostConditionsV4.Builder builder) {
+          builder.addKeyCondition(startsWith, "");
+          return new PostPolicyV4.ConditionV4(startsWith, "key", "");
+        }
+
+        @Override
+        public String toString() {
+          return "addKeyCondition()";
+        }
+      },
+      new ConditionTest() {
+        @Override
+        public PostPolicyV4.ConditionV4 addCondition(
+            PostPolicyV4.PostConditionsV4.Builder builder) {
+          builder.addSuccessActionRedirectUrlCondition(eq, "fail");
+          return new PostPolicyV4.ConditionV4(eq, "success_action_redirect", "fail");
+        }
+
+        @Override
+        public String toString() {
+          return "addSuccessActionRedirectUrlCondition()";
+        }
+      },
+    };
+
+    for (ConditionTest testCase : cases) {
+      PostPolicyV4.PostConditionsV4.Builder builder = PostPolicyV4.PostConditionsV4.newBuilder();
+      PostPolicyV4.ConditionV4 expected = testCase.addCondition(builder);
+      Set<PostPolicyV4.ConditionV4> conditions = builder.build().getConditions();
+      assertEquals("size", 1, conditions.size());
+      PostPolicyV4.ConditionV4 actual = conditions.toArray(new PostPolicyV4.ConditionV4[1])[0];
+      assertEquals(testCase.toString(), expected, actual);
+    }
+  }
+
+  @Test
+  public void testPostConditionsV4_addConditionFail() {
+    final PostPolicyV4.PostConditionsV4.Builder builder =
+        PostPolicyV4.PostConditionsV4.newBuilder();
+    final PostPolicyV4.ConditionV4Type range = PostPolicyV4.ConditionV4Type.CONTENT_LENGTH_RANGE;
+
+    Callable[] cases = {
+      new Callable<Void>() {
+        @Override
+        public Void call() {
+          builder.addAclCondition(range, "");
+          return null;
+        }
+
+        @Override
+        public String toString() {
+          return "acl";
+        }
+      },
+      new Callable<Void>() {
+        @Override
+        public Void call() {
+          builder.addBucketCondition(range, "");
+          return null;
+        }
+
+        @Override
+        public String toString() {
+          return "bucket";
+        }
+      },
+      new Callable<Void>() {
+        @Override
+        public Void call() {
+          builder.addCacheControlCondition(range, "");
+          return null;
+        }
+
+        @Override
+        public String toString() {
+          return "cache-control";
+        }
+      },
+      new Callable<Void>() {
+        @Override
+        public Void call() {
+          builder.addContentDispositionCondition(range, "");
+          return null;
+        }
+
+        @Override
+        public String toString() {
+          return "content-disposition";
+        }
+      },
+      new Callable<Void>() {
+        @Override
+        public Void call() {
+          builder.addContentEncodingCondition(range, "");
+          return null;
+        }
+
+        @Override
+        public String toString() {
+          return "content-encoding";
+        }
+      },
+      new Callable<Void>() {
+        @Override
+        public Void call() {
+          builder.addContentTypeCondition(range, "");
+          return null;
+        }
+
+        @Override
+        public String toString() {
+          return "content-type";
+        }
+      },
+      new Callable<Void>() {
+        @Override
+        public Void call() {
+          builder.addKeyCondition(range, "");
+          return null;
+        }
+
+        @Override
+        public String toString() {
+          return "key";
+        }
+      },
+      new Callable<Void>() {
+        @Override
+        public Void call() {
+          builder.addSuccessActionRedirectUrlCondition(range, "");
+          return null;
+        }
+
+        @Override
+        public String toString() {
+          return "success_action_redirect";
+        }
+      },
+    };
+
+    for (Callable testCase : cases) {
+      try {
+        testCase.call();
+        fail();
+      } catch (Exception e) {
+        String expected =
+            "java.lang.IllegalArgumentException: Field "
+                + testCase
+                + " can't use content-length-range";
+        assertEquals(expected, e.toString());
+      }
+    }
+    assertTrue(builder.build().getConditions().isEmpty());
+  }
+
+  @Test
+  public void testPostConditionsV4_toString() {
+    PostPolicyV4.PostConditionsV4.Builder builder = PostPolicyV4.PostConditionsV4.newBuilder();
+    builder.addKeyCondition(PostPolicyV4.ConditionV4Type.MATCHES, "test-object");
+    builder.addAclCondition(PostPolicyV4.ConditionV4Type.STARTS_WITH, "public");
+    builder.addContentLengthRangeCondition(246, 266);
+
+    Set<String> toStringSet = new HashSet<>();
+    for (PostPolicyV4.ConditionV4 conditionV4 : builder.build().getConditions()) {
+      toStringSet.add(conditionV4.toString());
+    }
+    assertEquals(3, toStringSet.size());
+
+    String[] expectedStrings = {
+      "[\"eq\", \"$key\", \"test-object\"]",
+      "[\"starts-with\", \"$acl\", \"public\"]",
+      "[\"content-length-range\", 246, 266]"
+    };
+
+    for (String expected : expectedStrings) {
+      assertTrue(expected + "/" + toStringSet, toStringSet.contains(expected));
+    }
+  }
+
+  @Test
+  public void testPostPolicyV4Document_of_toJson() {
+    PostPolicyV4.PostConditionsV4 emptyConditions =
+        PostPolicyV4.PostConditionsV4.newBuilder().build();
+    PostPolicyV4.PostPolicyV4Document emptyDocument =
+        PostPolicyV4.PostPolicyV4Document.of("", emptyConditions);
+    String emptyJson = emptyDocument.toJson();
+    assertEquals(emptyJson, "{\"conditions\":[],\"expiration\":\"\"}");
+
+    PostPolicyV4.PostConditionsV4 postConditionsV4 =
+        PostPolicyV4.PostConditionsV4.newBuilder()
+            .addBucketCondition(PostPolicyV4.ConditionV4Type.MATCHES, "my-bucket")
+            .addKeyCondition(PostPolicyV4.ConditionV4Type.STARTS_WITH, "")
+            .addContentLengthRangeCondition(1, 1000)
+            .build();
+
+    String expiration = dateFormat.format(System.currentTimeMillis());
+    PostPolicyV4.PostPolicyV4Document document =
+        PostPolicyV4.PostPolicyV4Document.of(expiration, postConditionsV4);
+    String json = document.toJson();
+    assertEquals(
+        json,
+        "{\"conditions\":[{\"bucket\":\"my-bucket\"},[\"starts-with\",\"$key\",\"\"],[\"content-length-range\",1,1000]],\"expiration\":\""
+            + expiration
+            + "\"}");
+  }
+}
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplMockitoTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplMockitoTest.java
index 9eaefc66..4efdda4d 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplMockitoTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplMockitoTest.java
@@ -19,6 +19,7 @@ package com.google.cloud.storage;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -27,6 +28,7 @@ import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 
 import com.google.api.core.ApiClock;
+import com.google.api.gax.paging.Page;
 import com.google.api.services.storage.model.StorageObject;
 import com.google.cloud.Identity;
 import com.google.cloud.Policy;
@@ -38,6 +40,7 @@ import com.google.cloud.storage.spi.StorageRpcFactory;
 import com.google.cloud.storage.spi.v1.StorageRpc;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
 import com.google.common.io.BaseEncoding;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
@@ -938,8 +941,6 @@ public class StorageImplMockitoTest {
   @Test
   @SuppressWarnings({"unchecked", "deprecation"})
   public void testCreateBlobFromStreamRetryableException() throws IOException {
-    ArgumentCaptor<ByteArrayInputStream> capturedStream =
-        ArgumentCaptor.forClass(ByteArrayInputStream.class);
 
     ByteArrayInputStream fileStream = new ByteArrayInputStream(BLOB_CONTENT);
 
@@ -1144,6 +1145,330 @@ public class StorageImplMockitoTest {
     assertEquals(Blob.fromPb(storage, storageObject), blob);
   }
 
+  @Test
+  public void testListBuckets() {
+    String cursor = "cursor";
+    ImmutableList<BucketInfo> bucketInfoList = ImmutableList.of(BUCKET_INFO1, BUCKET_INFO2);
+    Tuple<String, Iterable<com.google.api.services.storage.model.Bucket>> result =
+        Tuple.of(cursor, Iterables.transform(bucketInfoList, BucketInfo.TO_PB_FUNCTION));
+
+    doReturn(result)
+        .doThrow(UNEXPECTED_CALL_EXCEPTION)
+        .when(storageRpcMock)
+        .list(EMPTY_RPC_OPTIONS);
+
+    initializeService();
+    ImmutableList<Bucket> bucketList = ImmutableList.of(expectedBucket1, expectedBucket2);
+    Page<Bucket> page = storage.list();
+    assertEquals(cursor, page.getNextPageToken());
+    assertArrayEquals(bucketList.toArray(), Iterables.toArray(page.getValues(), Bucket.class));
+  }
+
+  @Test
+  public void testListBucketsEmpty() {
+    doReturn(Tuple.<String, Iterable<com.google.api.services.storage.model.Bucket>>of(null, null))
+        .doThrow(UNEXPECTED_CALL_EXCEPTION)
+        .when(storageRpcMock)
+        .list(EMPTY_RPC_OPTIONS);
+
+    initializeService();
+    Page<Bucket> page = storage.list();
+    assertNull(page.getNextPageToken());
+    assertArrayEquals(
+        ImmutableList.of().toArray(), Iterables.toArray(page.getValues(), Bucket.class));
+  }
+
+  @Test
+  public void testListBucketsWithOptions() {
+    String cursor = "cursor";
+    ImmutableList<BucketInfo> bucketInfoList = ImmutableList.of(BUCKET_INFO1, BUCKET_INFO2);
+    Tuple<String, Iterable<com.google.api.services.storage.model.Bucket>> result =
+        Tuple.of(cursor, Iterables.transform(bucketInfoList, BucketInfo.TO_PB_FUNCTION));
+
+    doReturn(result)
+        .doThrow(UNEXPECTED_CALL_EXCEPTION)
+        .when(storageRpcMock)
+        .list(BUCKET_LIST_OPTIONS);
+
+    initializeService();
+    ImmutableList<Bucket> bucketList = ImmutableList.of(expectedBucket1, expectedBucket2);
+    Page<Bucket> page = storage.list(BUCKET_LIST_PAGE_SIZE, BUCKET_LIST_PREFIX);
+    assertEquals(cursor, page.getNextPageToken());
+    assertArrayEquals(bucketList.toArray(), Iterables.toArray(page.getValues(), Bucket.class));
+  }
+
+  @Test
+  public void testListBucketsWithSelectedFields() {
+    String cursor = "cursor";
+    ArgumentCaptor<Map<StorageRpc.Option, Object>> capturedOptions =
+        ArgumentCaptor.forClass(Map.class);
+
+    ImmutableList<BucketInfo> bucketInfoList = ImmutableList.of(BUCKET_INFO1, BUCKET_INFO2);
+    Tuple<String, Iterable<com.google.api.services.storage.model.Bucket>> result =
+        Tuple.of(cursor, Iterables.transform(bucketInfoList, BucketInfo.TO_PB_FUNCTION));
+
+    doReturn(result)
+        .doThrow(UNEXPECTED_CALL_EXCEPTION)
+        .when(storageRpcMock)
+        .list(capturedOptions.capture());
+    initializeService();
+    ImmutableList<Bucket> bucketList = ImmutableList.of(expectedBucket1, expectedBucket2);
+    Page<Bucket> page = storage.list(BUCKET_LIST_FIELDS);
+    String selector = (String) capturedOptions.getValue().get(BUCKET_LIST_FIELDS.getRpcOption());
+    assertTrue(selector.contains("items("));
+    assertTrue(selector.contains("name"));
+    assertTrue(selector.contains("acl"));
+    assertTrue(selector.contains("location"));
+    assertTrue(selector.contains("nextPageToken"));
+    assertTrue(selector.endsWith(")"));
+    assertEquals(38, selector.length());
+    assertEquals(cursor, page.getNextPageToken());
+    assertArrayEquals(bucketList.toArray(), Iterables.toArray(page.getValues(), Bucket.class));
+  }
+
+  @Test
+  public void testListBucketsWithEmptyFields() {
+    String cursor = "cursor";
+    ArgumentCaptor<Map<StorageRpc.Option, Object>> capturedOptions =
+        ArgumentCaptor.forClass(Map.class);
+    ImmutableList<BucketInfo> bucketInfoList = ImmutableList.of(BUCKET_INFO1, BUCKET_INFO2);
+    Tuple<String, Iterable<com.google.api.services.storage.model.Bucket>> result =
+        Tuple.of(cursor, Iterables.transform(bucketInfoList, BucketInfo.TO_PB_FUNCTION));
+
+    doReturn(result)
+        .doThrow(UNEXPECTED_CALL_EXCEPTION)
+        .when(storageRpcMock)
+        .list(capturedOptions.capture());
+    initializeService();
+    ImmutableList<Bucket> bucketList = ImmutableList.of(expectedBucket1, expectedBucket2);
+    Page<Bucket> page = storage.list(BUCKET_LIST_EMPTY_FIELDS);
+    String selector =
+        (String) capturedOptions.getValue().get(BUCKET_LIST_EMPTY_FIELDS.getRpcOption());
+    assertTrue(selector.contains("items("));
+    assertTrue(selector.contains("name"));
+    assertTrue(selector.contains("nextPageToken"));
+    assertTrue(selector.endsWith(")"));
+    assertEquals(25, selector.length());
+    assertEquals(cursor, page.getNextPageToken());
+    assertArrayEquals(bucketList.toArray(), Iterables.toArray(page.getValues(), Bucket.class));
+  }
+
+  @Test
+  public void testListBucketsWithException() {
+    doThrow(STORAGE_FAILURE).when(storageRpcMock).list(EMPTY_RPC_OPTIONS);
+    initializeService();
+    try {
+      storage.list();
+      fail();
+    } catch (StorageException e) {
+      assertEquals(STORAGE_FAILURE.toString(), e.getMessage());
+    }
+  }
+
+  @Test
+  public void testListBlobs() {
+    String cursor = "cursor";
+    ImmutableList<BlobInfo> blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2);
+    Tuple<String, Iterable<com.google.api.services.storage.model.StorageObject>> result =
+        Tuple.of(cursor, Iterables.transform(blobInfoList, BlobInfo.INFO_TO_PB_FUNCTION));
+
+    doReturn(result)
+        .doThrow(UNEXPECTED_CALL_EXCEPTION)
+        .when(storageRpcMock)
+        .list(BUCKET_NAME1, EMPTY_RPC_OPTIONS);
+
+    initializeService();
+    ImmutableList<Blob> blobList = ImmutableList.of(expectedBlob1, expectedBlob2);
+    Page<Blob> page = storage.list(BUCKET_NAME1);
+    assertEquals(cursor, page.getNextPageToken());
+    assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class));
+  }
+
+  @Test
+  public void testListBlobsEmpty() {
+    doReturn(
+            Tuple.<String, Iterable<com.google.api.services.storage.model.StorageObject>>of(
+                null, null))
+        .doThrow(UNEXPECTED_CALL_EXCEPTION)
+        .when(storageRpcMock)
+        .list(BUCKET_NAME1, EMPTY_RPC_OPTIONS);
+
+    initializeService();
+    Page<Blob> page = storage.list(BUCKET_NAME1);
+    assertNull(page.getNextPageToken());
+    assertArrayEquals(
+        ImmutableList.of().toArray(), Iterables.toArray(page.getValues(), Blob.class));
+  }
+
+  @Test
+  public void testListBlobsWithOptions() {
+    String cursor = "cursor";
+    ImmutableList<BlobInfo> blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2);
+    Tuple<String, Iterable<com.google.api.services.storage.model.StorageObject>> result =
+        Tuple.of(cursor, Iterables.transform(blobInfoList, BlobInfo.INFO_TO_PB_FUNCTION));
+    doReturn(result)
+        .doThrow(UNEXPECTED_CALL_EXCEPTION)
+        .when(storageRpcMock)
+        .list(BUCKET_NAME1, BLOB_LIST_OPTIONS);
+    initializeService();
+    ImmutableList<Blob> blobList = ImmutableList.of(expectedBlob1, expectedBlob2);
+    Page<Blob> page =
+        storage.list(BUCKET_NAME1, BLOB_LIST_PAGE_SIZE, BLOB_LIST_PREFIX, BLOB_LIST_VERSIONS);
+    assertEquals(cursor, page.getNextPageToken());
+    assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class));
+  }
+
+  @Test
+  public void testListBlobsWithSelectedFields() {
+    String cursor = "cursor";
+    ArgumentCaptor<Map<StorageRpc.Option, Object>> capturedOptions =
+        ArgumentCaptor.forClass(Map.class);
+    ImmutableList<BlobInfo> blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2);
+    Tuple<String, Iterable<com.google.api.services.storage.model.StorageObject>> result =
+        Tuple.of(cursor, Iterables.transform(blobInfoList, BlobInfo.INFO_TO_PB_FUNCTION));
+    doReturn(result)
+        .doThrow(UNEXPECTED_CALL_EXCEPTION)
+        .when(storageRpcMock)
+        .list(Mockito.eq(BUCKET_NAME1), capturedOptions.capture());
+
+    initializeService();
+    ImmutableList<Blob> blobList = ImmutableList.of(expectedBlob1, expectedBlob2);
+    Page<Blob> page =
+        storage.list(BUCKET_NAME1, BLOB_LIST_PAGE_SIZE, BLOB_LIST_PREFIX, BLOB_LIST_FIELDS);
+    assertEquals(
+        BLOB_LIST_PAGE_SIZE.getValue(),
+        capturedOptions.getValue().get(BLOB_LIST_PAGE_SIZE.getRpcOption()));
+    assertEquals(
+        BLOB_LIST_PREFIX.getValue(),
+        capturedOptions.getValue().get(BLOB_LIST_PREFIX.getRpcOption()));
+    String selector = (String) capturedOptions.getValue().get(BLOB_LIST_FIELDS.getRpcOption());
+    assertTrue(selector.contains("prefixes"));
+    assertTrue(selector.contains("items("));
+    assertTrue(selector.contains("bucket"));
+    assertTrue(selector.contains("name"));
+    assertTrue(selector.contains("contentType"));
+    assertTrue(selector.contains("md5Hash"));
+    assertTrue(selector.contains("nextPageToken"));
+    assertTrue(selector.endsWith(")"));
+    assertEquals(61, selector.length());
+    assertEquals(cursor, page.getNextPageToken());
+    assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class));
+  }
+
+  @Test
+  public void testListBlobsWithEmptyFields() {
+    String cursor = "cursor";
+    ArgumentCaptor<Map<StorageRpc.Option, Object>> capturedOptions =
+        ArgumentCaptor.forClass(Map.class);
+    ImmutableList<BlobInfo> blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2);
+    Tuple<String, Iterable<com.google.api.services.storage.model.StorageObject>> result =
+        Tuple.of(cursor, Iterables.transform(blobInfoList, BlobInfo.INFO_TO_PB_FUNCTION));
+    doReturn(result)
+        .doThrow(UNEXPECTED_CALL_EXCEPTION)
+        .when(storageRpcMock)
+        .list(Mockito.eq(BUCKET_NAME1), capturedOptions.capture());
+
+    initializeService();
+    ImmutableList<Blob> blobList = ImmutableList.of(expectedBlob1, expectedBlob2);
+    Page<Blob> page =
+        storage.list(BUCKET_NAME1, BLOB_LIST_PAGE_SIZE, BLOB_LIST_PREFIX, BLOB_LIST_EMPTY_FIELDS);
+    assertEquals(
+        BLOB_LIST_PAGE_SIZE.getValue(),
+        capturedOptions.getValue().get(BLOB_LIST_PAGE_SIZE.getRpcOption()));
+    assertEquals(
+        BLOB_LIST_PREFIX.getValue(),
+        capturedOptions.getValue().get(BLOB_LIST_PREFIX.getRpcOption()));
+    String selector =
+        (String) capturedOptions.getValue().get(BLOB_LIST_EMPTY_FIELDS.getRpcOption());
+    assertTrue(selector.contains("prefixes"));
+    assertTrue(selector.contains("items("));
+    assertTrue(selector.contains("bucket"));
+    assertTrue(selector.contains("name"));
+    assertTrue(selector.contains("nextPageToken"));
+    assertTrue(selector.endsWith(")"));
+    assertEquals(41, selector.length());
+    assertEquals(cursor, page.getNextPageToken());
+    assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class));
+  }
+
+  @Test
+  public void testListBlobsCurrentDirectory() {
+    String cursor = "cursor";
+    Map<StorageRpc.Option, ?> options = ImmutableMap.of(StorageRpc.Option.DELIMITER, "/");
+    ImmutableList<BlobInfo> blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2);
+    Tuple<String, Iterable<com.google.api.services.storage.model.StorageObject>> result =
+        Tuple.of(cursor, Iterables.transform(blobInfoList, BlobInfo.INFO_TO_PB_FUNCTION));
+    doReturn(result)
+        .doThrow(UNEXPECTED_CALL_EXCEPTION)
+        .when(storageRpcMock)
+        .list(BUCKET_NAME1, options);
+
+    initializeService();
+    ImmutableList<Blob> blobList = ImmutableList.of(expectedBlob1, expectedBlob2);
+    Page<Blob> page = storage.list(BUCKET_NAME1, Storage.BlobListOption.currentDirectory());
+    assertEquals(cursor, page.getNextPageToken());
+    assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class));
+  }
+
+  @Test
+  public void testListBlobsDelimiter() {
+    String cursor = "cursor";
+    String delimiter = "/";
+    Map<StorageRpc.Option, ?> options = ImmutableMap.of(StorageRpc.Option.DELIMITER, delimiter);
+    ImmutableList<BlobInfo> blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2);
+    Tuple<String, Iterable<com.google.api.services.storage.model.StorageObject>> result =
+        Tuple.of(cursor, Iterables.transform(blobInfoList, BlobInfo.INFO_TO_PB_FUNCTION));
+    doReturn(result)
+        .doThrow(UNEXPECTED_CALL_EXCEPTION)
+        .when(storageRpcMock)
+        .list(BUCKET_NAME1, options);
+
+    initializeService();
+    ImmutableList<Blob> blobList = ImmutableList.of(expectedBlob1, expectedBlob2);
+    Page<Blob> page = storage.list(BUCKET_NAME1, Storage.BlobListOption.delimiter(delimiter));
+    assertEquals(cursor, page.getNextPageToken());
+    assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class));
+  }
+
+  @Test
+  public void testListBlobsWithOffset() {
+    String cursor = "cursor";
+    String startOffset = "startOffset";
+    String endOffset = "endOffset";
+    Map<StorageRpc.Option, ?> options =
+        ImmutableMap.of(
+            StorageRpc.Option.START_OFF_SET, startOffset, StorageRpc.Option.END_OFF_SET, endOffset);
+    ImmutableList<BlobInfo> blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2);
+    Tuple<String, Iterable<com.google.api.services.storage.model.StorageObject>> result =
+        Tuple.of(cursor, Iterables.transform(blobInfoList, BlobInfo.INFO_TO_PB_FUNCTION));
+    doReturn(result)
+        .doThrow(UNEXPECTED_CALL_EXCEPTION)
+        .when(storageRpcMock)
+        .list(BUCKET_NAME1, options);
+
+    initializeService();
+    ImmutableList<Blob> blobList = ImmutableList.of(expectedBlob1, expectedBlob2);
+    Page<Blob> page =
+        storage.list(
+            BUCKET_NAME1,
+            Storage.BlobListOption.startOffset(startOffset),
+            Storage.BlobListOption.endOffset(endOffset));
+    assertEquals(cursor, page.getNextPageToken());
+    assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class));
+  }
+
+  @Test
+  public void testListBlobsWithException() {
+    doThrow(STORAGE_FAILURE).when(storageRpcMock).list(BUCKET_NAME1, EMPTY_RPC_OPTIONS);
+    initializeService();
+    try {
+      storage.list(BUCKET_NAME1);
+      fail();
+    } catch (StorageException e) {
+      assertEquals(STORAGE_FAILURE.toString(), e.getMessage());
+    }
+  }
+
   private void verifyChannelRead(ReadChannel channel, byte[] bytes) throws IOException {
     assertNotNull(channel);
     assertTrue(channel.isOpen());
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java
index 1b78780f..590fe37a 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java
@@ -29,8 +29,8 @@ import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 
 import com.google.api.client.googleapis.json.GoogleJsonError;
+import com.google.api.client.util.DateTime;
 import com.google.api.core.ApiClock;
-import com.google.api.gax.paging.Page;
 import com.google.api.services.storage.model.Policy.Bindings;
 import com.google.api.services.storage.model.StorageObject;
 import com.google.api.services.storage.model.TestIamPermissionsResponse;
@@ -38,7 +38,6 @@ import com.google.auth.oauth2.ServiceAccountCredentials;
 import com.google.cloud.Identity;
 import com.google.cloud.Policy;
 import com.google.cloud.ServiceOptions;
-import com.google.cloud.Tuple;
 import com.google.cloud.WriteChannel;
 import com.google.cloud.storage.Acl.Project;
 import com.google.cloud.storage.Acl.Project.ProjectRole;
@@ -54,7 +53,6 @@ import com.google.cloud.storage.spi.v1.RpcBatch;
 import com.google.cloud.storage.spi.v1.StorageRpc;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Iterables;
 import com.google.common.io.BaseEncoding;
 import java.io.UnsupportedEncodingException;
 import java.net.MalformedURLException;
@@ -428,267 +426,6 @@ public class StorageImplTest {
     expectedBucket3 = new Bucket(storage, new BucketInfo.BuilderImpl(BUCKET_INFO3));
   }
 
-  @Test
-  public void testListBuckets() {
-    String cursor = "cursor";
-    ImmutableList<BucketInfo> bucketInfoList = ImmutableList.of(BUCKET_INFO1, BUCKET_INFO2);
-    Tuple<String, Iterable<com.google.api.services.storage.model.Bucket>> result =
-        Tuple.of(cursor, Iterables.transform(bucketInfoList, BucketInfo.TO_PB_FUNCTION));
-    EasyMock.expect(storageRpcMock.list(EMPTY_RPC_OPTIONS)).andReturn(result);
-    EasyMock.replay(storageRpcMock);
-    initializeService();
-    ImmutableList<Bucket> bucketList = ImmutableList.of(expectedBucket1, expectedBucket2);
-    Page<Bucket> page = storage.list();
-    assertEquals(cursor, page.getNextPageToken());
-    assertArrayEquals(bucketList.toArray(), Iterables.toArray(page.getValues(), Bucket.class));
-  }
-
-  @Test
-  public void testListBucketsEmpty() {
-    EasyMock.expect(storageRpcMock.list(EMPTY_RPC_OPTIONS))
-        .andReturn(
-            Tuple.<String, Iterable<com.google.api.services.storage.model.Bucket>>of(null, null));
-    EasyMock.replay(storageRpcMock);
-    initializeService();
-    Page<Bucket> page = storage.list();
-    assertNull(page.getNextPageToken());
-    assertArrayEquals(
-        ImmutableList.of().toArray(), Iterables.toArray(page.getValues(), Bucket.class));
-  }
-
-  @Test
-  public void testListBucketsWithOptions() {
-    String cursor = "cursor";
-    ImmutableList<BucketInfo> bucketInfoList = ImmutableList.of(BUCKET_INFO1, BUCKET_INFO2);
-    Tuple<String, Iterable<com.google.api.services.storage.model.Bucket>> result =
-        Tuple.of(cursor, Iterables.transform(bucketInfoList, BucketInfo.TO_PB_FUNCTION));
-    EasyMock.expect(storageRpcMock.list(BUCKET_LIST_OPTIONS)).andReturn(result);
-    EasyMock.replay(storageRpcMock);
-    initializeService();
-    ImmutableList<Bucket> bucketList = ImmutableList.of(expectedBucket1, expectedBucket2);
-    Page<Bucket> page = storage.list(BUCKET_LIST_PAGE_SIZE, BUCKET_LIST_PREFIX);
-    assertEquals(cursor, page.getNextPageToken());
-    assertArrayEquals(bucketList.toArray(), Iterables.toArray(page.getValues(), Bucket.class));
-  }
-
-  @Test
-  public void testListBucketsWithSelectedFields() {
-    String cursor = "cursor";
-    Capture<Map<StorageRpc.Option, Object>> capturedOptions = Capture.newInstance();
-    ImmutableList<BucketInfo> bucketInfoList = ImmutableList.of(BUCKET_INFO1, BUCKET_INFO2);
-    Tuple<String, Iterable<com.google.api.services.storage.model.Bucket>> result =
-        Tuple.of(cursor, Iterables.transform(bucketInfoList, BucketInfo.TO_PB_FUNCTION));
-    EasyMock.expect(storageRpcMock.list(EasyMock.capture(capturedOptions))).andReturn(result);
-    EasyMock.replay(storageRpcMock);
-    initializeService();
-    ImmutableList<Bucket> bucketList = ImmutableList.of(expectedBucket1, expectedBucket2);
-    Page<Bucket> page = storage.list(BUCKET_LIST_FIELDS);
-    String selector = (String) capturedOptions.getValue().get(BUCKET_LIST_FIELDS.getRpcOption());
-    assertTrue(selector.contains("items("));
-    assertTrue(selector.contains("name"));
-    assertTrue(selector.contains("acl"));
-    assertTrue(selector.contains("location"));
-    assertTrue(selector.contains("nextPageToken"));
-    assertTrue(selector.endsWith(")"));
-    assertEquals(38, selector.length());
-    assertEquals(cursor, page.getNextPageToken());
-    assertArrayEquals(bucketList.toArray(), Iterables.toArray(page.getValues(), Bucket.class));
-  }
-
-  @Test
-  public void testListBucketsWithEmptyFields() {
-    String cursor = "cursor";
-    Capture<Map<StorageRpc.Option, Object>> capturedOptions = Capture.newInstance();
-    ImmutableList<BucketInfo> bucketInfoList = ImmutableList.of(BUCKET_INFO1, BUCKET_INFO2);
-    Tuple<String, Iterable<com.google.api.services.storage.model.Bucket>> result =
-        Tuple.of(cursor, Iterables.transform(bucketInfoList, BucketInfo.TO_PB_FUNCTION));
-    EasyMock.expect(storageRpcMock.list(EasyMock.capture(capturedOptions))).andReturn(result);
-    EasyMock.replay(storageRpcMock);
-    initializeService();
-    ImmutableList<Bucket> bucketList = ImmutableList.of(expectedBucket1, expectedBucket2);
-    Page<Bucket> page = storage.list(BUCKET_LIST_EMPTY_FIELDS);
-    String selector =
-        (String) capturedOptions.getValue().get(BUCKET_LIST_EMPTY_FIELDS.getRpcOption());
-    assertTrue(selector.contains("items("));
-    assertTrue(selector.contains("name"));
-    assertTrue(selector.contains("nextPageToken"));
-    assertTrue(selector.endsWith(")"));
-    assertEquals(25, selector.length());
-    assertEquals(cursor, page.getNextPageToken());
-    assertArrayEquals(bucketList.toArray(), Iterables.toArray(page.getValues(), Bucket.class));
-  }
-
-  @Test
-  public void testListBlobs() {
-    String cursor = "cursor";
-    ImmutableList<BlobInfo> blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2);
-    Tuple<String, Iterable<com.google.api.services.storage.model.StorageObject>> result =
-        Tuple.of(cursor, Iterables.transform(blobInfoList, BlobInfo.INFO_TO_PB_FUNCTION));
-    EasyMock.expect(storageRpcMock.list(BUCKET_NAME1, EMPTY_RPC_OPTIONS)).andReturn(result);
-    EasyMock.replay(storageRpcMock);
-    initializeService();
-    ImmutableList<Blob> blobList = ImmutableList.of(expectedBlob1, expectedBlob2);
-    Page<Blob> page = storage.list(BUCKET_NAME1);
-    assertEquals(cursor, page.getNextPageToken());
-    assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class));
-  }
-
-  @Test
-  public void testListBlobsEmpty() {
-    EasyMock.expect(storageRpcMock.list(BUCKET_NAME1, EMPTY_RPC_OPTIONS))
-        .andReturn(
-            Tuple.<String, Iterable<com.google.api.services.storage.model.StorageObject>>of(
-                null, null));
-    EasyMock.replay(storageRpcMock);
-    initializeService();
-    Page<Blob> page = storage.list(BUCKET_NAME1);
-    assertNull(page.getNextPageToken());
-    assertArrayEquals(
-        ImmutableList.of().toArray(), Iterables.toArray(page.getValues(), Blob.class));
-  }
-
-  @Test
-  public void testListBlobsWithOptions() {
-    String cursor = "cursor";
-    ImmutableList<BlobInfo> blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2);
-    Tuple<String, Iterable<com.google.api.services.storage.model.StorageObject>> result =
-        Tuple.of(cursor, Iterables.transform(blobInfoList, BlobInfo.INFO_TO_PB_FUNCTION));
-    EasyMock.expect(storageRpcMock.list(BUCKET_NAME1, BLOB_LIST_OPTIONS)).andReturn(result);
-    EasyMock.replay(storageRpcMock);
-    initializeService();
-    ImmutableList<Blob> blobList = ImmutableList.of(expectedBlob1, expectedBlob2);
-    Page<Blob> page =
-        storage.list(BUCKET_NAME1, BLOB_LIST_PAGE_SIZE, BLOB_LIST_PREFIX, BLOB_LIST_VERSIONS);
-    assertEquals(cursor, page.getNextPageToken());
-    assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class));
-  }
-
-  @Test
-  public void testListBlobsWithSelectedFields() {
-    String cursor = "cursor";
-    Capture<Map<StorageRpc.Option, Object>> capturedOptions = Capture.newInstance();
-    ImmutableList<BlobInfo> blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2);
-    Tuple<String, Iterable<com.google.api.services.storage.model.StorageObject>> result =
-        Tuple.of(cursor, Iterables.transform(blobInfoList, BlobInfo.INFO_TO_PB_FUNCTION));
-    EasyMock.expect(
-            storageRpcMock.list(EasyMock.eq(BUCKET_NAME1), EasyMock.capture(capturedOptions)))
-        .andReturn(result);
-    EasyMock.replay(storageRpcMock);
-    initializeService();
-    ImmutableList<Blob> blobList = ImmutableList.of(expectedBlob1, expectedBlob2);
-    Page<Blob> page =
-        storage.list(BUCKET_NAME1, BLOB_LIST_PAGE_SIZE, BLOB_LIST_PREFIX, BLOB_LIST_FIELDS);
-    assertEquals(
-        BLOB_LIST_PAGE_SIZE.getValue(),
-        capturedOptions.getValue().get(BLOB_LIST_PAGE_SIZE.getRpcOption()));
-    assertEquals(
-        BLOB_LIST_PREFIX.getValue(),
-        capturedOptions.getValue().get(BLOB_LIST_PREFIX.getRpcOption()));
-    String selector = (String) capturedOptions.getValue().get(BLOB_LIST_FIELDS.getRpcOption());
-    assertTrue(selector.contains("prefixes"));
-    assertTrue(selector.contains("items("));
-    assertTrue(selector.contains("bucket"));
-    assertTrue(selector.contains("name"));
-    assertTrue(selector.contains("contentType"));
-    assertTrue(selector.contains("md5Hash"));
-    assertTrue(selector.contains("nextPageToken"));
-    assertTrue(selector.endsWith(")"));
-    assertEquals(61, selector.length());
-    assertEquals(cursor, page.getNextPageToken());
-    assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class));
-  }
-
-  @Test
-  public void testListBlobsWithEmptyFields() {
-    String cursor = "cursor";
-    Capture<Map<StorageRpc.Option, Object>> capturedOptions = Capture.newInstance();
-    ImmutableList<BlobInfo> blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2);
-    Tuple<String, Iterable<com.google.api.services.storage.model.StorageObject>> result =
-        Tuple.of(cursor, Iterables.transform(blobInfoList, BlobInfo.INFO_TO_PB_FUNCTION));
-    EasyMock.expect(
-            storageRpcMock.list(EasyMock.eq(BUCKET_NAME1), EasyMock.capture(capturedOptions)))
-        .andReturn(result);
-    EasyMock.replay(storageRpcMock);
-    initializeService();
-    ImmutableList<Blob> blobList = ImmutableList.of(expectedBlob1, expectedBlob2);
-    Page<Blob> page =
-        storage.list(BUCKET_NAME1, BLOB_LIST_PAGE_SIZE, BLOB_LIST_PREFIX, BLOB_LIST_EMPTY_FIELDS);
-    assertEquals(
-        BLOB_LIST_PAGE_SIZE.getValue(),
-        capturedOptions.getValue().get(BLOB_LIST_PAGE_SIZE.getRpcOption()));
-    assertEquals(
-        BLOB_LIST_PREFIX.getValue(),
-        capturedOptions.getValue().get(BLOB_LIST_PREFIX.getRpcOption()));
-    String selector =
-        (String) capturedOptions.getValue().get(BLOB_LIST_EMPTY_FIELDS.getRpcOption());
-    assertTrue(selector.contains("prefixes"));
-    assertTrue(selector.contains("items("));
-    assertTrue(selector.contains("bucket"));
-    assertTrue(selector.contains("name"));
-    assertTrue(selector.contains("nextPageToken"));
-    assertTrue(selector.endsWith(")"));
-    assertEquals(41, selector.length());
-    assertEquals(cursor, page.getNextPageToken());
-    assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class));
-  }
-
-  @Test
-  public void testListBlobsCurrentDirectory() {
-    String cursor = "cursor";
-    Map<StorageRpc.Option, ?> options = ImmutableMap.of(StorageRpc.Option.DELIMITER, "/");
-    ImmutableList<BlobInfo> blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2);
-    Tuple<String, Iterable<com.google.api.services.storage.model.StorageObject>> result =
-        Tuple.of(cursor, Iterables.transform(blobInfoList, BlobInfo.INFO_TO_PB_FUNCTION));
-    EasyMock.expect(storageRpcMock.list(BUCKET_NAME1, options)).andReturn(result);
-    EasyMock.replay(storageRpcMock);
-    initializeService();
-    ImmutableList<Blob> blobList = ImmutableList.of(expectedBlob1, expectedBlob2);
-    Page<Blob> page = storage.list(BUCKET_NAME1, Storage.BlobListOption.currentDirectory());
-    assertEquals(cursor, page.getNextPageToken());
-    assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class));
-  }
-
-  @Test
-  public void testListBlobsDelimiter() {
-    String cursor = "cursor";
-    String delimiter = "/";
-    Map<StorageRpc.Option, ?> options = ImmutableMap.of(StorageRpc.Option.DELIMITER, delimiter);
-    ImmutableList<BlobInfo> blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2);
-    Tuple<String, Iterable<com.google.api.services.storage.model.StorageObject>> result =
-        Tuple.of(cursor, Iterables.transform(blobInfoList, BlobInfo.INFO_TO_PB_FUNCTION));
-    EasyMock.expect(storageRpcMock.list(BUCKET_NAME1, options)).andReturn(result);
-    EasyMock.replay(storageRpcMock);
-    initializeService();
-    ImmutableList<Blob> blobList = ImmutableList.of(expectedBlob1, expectedBlob2);
-    Page<Blob> page = storage.list(BUCKET_NAME1, Storage.BlobListOption.delimiter(delimiter));
-    assertEquals(cursor, page.getNextPageToken());
-    assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class));
-  }
-
-  @Test
-  public void testListBlobsWithOffset() {
-    String cursor = "cursor";
-    String startOffset = "startOffset";
-    String endOffset = "endOffset";
-    Map<StorageRpc.Option, ?> options =
-        ImmutableMap.of(
-            StorageRpc.Option.START_OFF_SET, startOffset, StorageRpc.Option.END_OFF_SET, endOffset);
-    ImmutableList<BlobInfo> blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2);
-    Tuple<String, Iterable<com.google.api.services.storage.model.StorageObject>> result =
-        Tuple.of(cursor, Iterables.transform(blobInfoList, BlobInfo.INFO_TO_PB_FUNCTION));
-    EasyMock.expect(storageRpcMock.list(BUCKET_NAME1, options)).andReturn(result);
-    EasyMock.replay(storageRpcMock);
-    initializeService();
-    ImmutableList<Blob> blobList = ImmutableList.of(expectedBlob1, expectedBlob2);
-    Page<Blob> page =
-        storage.list(
-            BUCKET_NAME1,
-            Storage.BlobListOption.startOffset(startOffset),
-            Storage.BlobListOption.endOffset(endOffset));
-    assertEquals(cursor, page.getNextPageToken());
-    assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class));
-  }
-
   @Test
   public void testUpdateBucket() {
     BucketInfo updatedBucketInfo = BUCKET_INFO1.toBuilder().setIndexPage("some-page").build();
@@ -2496,4 +2233,44 @@ public class StorageImplTest {
     assertEquals(outputFields.get("key"), "my-object");
     assertEquals("https://storage.googleapis.com/my-bucket/", policy.getUrl());
   }
+
+  @Test
+  public void testBucketLifecycleRules() {
+    BucketInfo bucketInfo =
+        BucketInfo.newBuilder("b")
+            .setLocation("us")
+            .setLifecycleRules(
+                ImmutableList.of(
+                    new BucketInfo.LifecycleRule(
+                        BucketInfo.LifecycleRule.LifecycleAction.newSetStorageClassAction(
+                            StorageClass.COLDLINE),
+                        BucketInfo.LifecycleRule.LifecycleCondition.newBuilder()
+                            .setAge(1)
+                            .setNumberOfNewerVersions(3)
+                            .setIsLive(false)
+                            .setCreatedBefore(new DateTime(System.currentTimeMillis()))
+                            .setMatchesStorageClass(ImmutableList.of(StorageClass.COLDLINE))
+                            .setDaysSinceNoncurrentTime(30)
+                            .setNoncurrentTimeBefore(new DateTime(System.currentTimeMillis()))
+                            .setCustomTimeBefore(new DateTime(System.currentTimeMillis()))
+                            .setDaysSinceCustomTime(30)
+                            .build())))
+            .build();
+    EasyMock.expect(
+            storageRpcMock.create(bucketInfo.toPb(), new HashMap<StorageRpc.Option, Object>()))
+        .andReturn(bucketInfo.toPb());
+    EasyMock.replay(storageRpcMock);
+    initializeService();
+    Bucket bucket = storage.create(bucketInfo);
+    BucketInfo.LifecycleRule lifecycleRule = bucket.getLifecycleRules().get(0);
+    assertEquals(3, lifecycleRule.getCondition().getNumberOfNewerVersions().intValue());
+    assertNotNull(lifecycleRule.getCondition().getCreatedBefore());
+    assertFalse(lifecycleRule.getCondition().getIsLive());
+    assertEquals(1, lifecycleRule.getCondition().getAge().intValue());
+    assertEquals(1, lifecycleRule.getCondition().getMatchesStorageClass().size());
+    assertEquals(30, lifecycleRule.getCondition().getDaysSinceNoncurrentTime().intValue());
+    assertNotNull(lifecycleRule.getCondition().getNoncurrentTimeBefore());
+    assertEquals(30, lifecycleRule.getCondition().getDaysSinceCustomTime().intValue());
+    assertNotNull(lifecycleRule.getCondition().getCustomTimeBefore());
+  }
 }
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java
index a741f3eb..925e3d6d 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java
@@ -456,6 +456,10 @@ public class ITStorageTest {
                             .setIsLive(false)
                             .setCreatedBefore(new DateTime(System.currentTimeMillis()))
                             .setMatchesStorageClass(ImmutableList.of(StorageClass.COLDLINE))
+                            .setDaysSinceNoncurrentTime(30)
+                            .setNoncurrentTimeBefore(new DateTime(System.currentTimeMillis()))
+                            .setCustomTimeBefore(new DateTime(System.currentTimeMillis()))
+                            .setDaysSinceCustomTime(30)
                             .build())))
             .build());
     Bucket remoteBucket =
@@ -472,6 +476,10 @@ public class ITStorageTest {
       assertFalse(lifecycleRule.getCondition().getIsLive());
       assertEquals(1, lifecycleRule.getCondition().getAge().intValue());
       assertEquals(1, lifecycleRule.getCondition().getMatchesStorageClass().size());
+      assertEquals(30, lifecycleRule.getCondition().getDaysSinceNoncurrentTime().intValue());
+      assertNotNull(lifecycleRule.getCondition().getNoncurrentTimeBefore());
+      assertEquals(30, lifecycleRule.getCondition().getDaysSinceCustomTime().intValue());
+      assertNotNull(lifecycleRule.getCondition().getCustomTimeBefore());
     } finally {
       storage.delete(lifecycleTestBucketName);
     }
@@ -519,9 +527,11 @@ public class ITStorageTest {
   @Test
   public void testCreateBlob() {
     String blobName = "test-create-blob";
-    BlobInfo blob = BlobInfo.newBuilder(BUCKET, blobName).build();
+    BlobInfo blob =
+        BlobInfo.newBuilder(BUCKET, blobName).setCustomTime(System.currentTimeMillis()).build();
     Blob remoteBlob = storage.create(blob, BLOB_BYTE_CONTENT);
     assertNotNull(remoteBlob);
+    assertNotNull(remoteBlob.getCustomTime());
     assertEquals(blob.getBucket(), remoteBlob.getBucket());
     assertEquals(blob.getName(), remoteBlob.getName());
     byte[] readBytes = storage.readAllBytes(BUCKET, blobName);