Repository: googleapis/java-storage
Pull request #2418 (2024-02-16 19:40:48)

test(deps): update dependency com.google.truth:truth to v1.4.1
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [com.google.truth:truth](https://togithub.com/google/truth) | `1.4.0` -> `1.4.1` | [![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.truth:truth/1.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.truth:truth/1.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.truth:truth/1.4.0/1.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.truth:truth/1.4.0/1.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>google/truth (com.google.truth:truth)</summary>

### [`v1.4.1`](https://togithub.com/google/truth/releases/tag/v1.4.1): 1.4.1

This release deprecates `Truth8`.

All its methods have become available on the main `Truth` class. In most cases, you can migrate your whole project mechanically: `git grep -l Truth8 | xargs perl -pi -e 's/\bTruth8\b/Truth/g;'`

While we do not plan to delete `Truth8`, we recommend migrating off it, at least if you static import `assertThat`: If you do not migrate, such static imports will become ambiguous in Truth 1.4.2, breaking your build.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/googleapis/java-storage).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4xNzMuMCIsInVwZGF0ZWRJblZlciI6IjM3LjE3My4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiJ9-->


forking-renovate[bot]: ### Edited/Blocked Notification

Renovate will not automatically rebase this PR, because it does not recognize the last commit author and assumes somebody else may have edited the PR.

You can manually request rebase by checking the rebase/retry box above.

 ⚠ **Warning**: custom changes will be lost.

Patch:
diff --git a/README.md b/README.md
index 269e3c08..b8a64db0 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,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>26.32.0</version>
+      <version>26.33.0</version>
       <type>pom</type>
       <scope>import</scope>
     </dependency>
@@ -42,7 +42,7 @@ If you are using Maven without the BOM, add this to your dependencies:
 <dependency>
   <groupId>com.google.cloud</groupId>
   <artifactId>google-cloud-storage</artifactId>
-  <version>2.33.0</version>
+  <version>2.34.0</version>
 </dependency>
 
 ```
@@ -50,20 +50,20 @@ If you are using Maven without the BOM, add this to your dependencies:
 If you are using Gradle 5.x or later, add this to your dependencies:
 
 ```Groovy
-implementation platform('com.google.cloud:libraries-bom:26.32.0')
+implementation platform('com.google.cloud:libraries-bom:26.33.0')
 
 implementation 'com.google.cloud:google-cloud-storage'
 ```
 If you are using Gradle without BOM, add this to your dependencies:
 
 ```Groovy
-implementation 'com.google.cloud:google-cloud-storage:2.33.0'
+implementation 'com.google.cloud:google-cloud-storage:2.34.0'
 ```
 
 If you are using SBT, add this to your dependencies:
 
 ```Scala
-libraryDependencies += "com.google.cloud" % "google-cloud-storage" % "2.33.0"
+libraryDependencies += "com.google.cloud" % "google-cloud-storage" % "2.34.0"
 ```
 <!-- {x-version-update-end} -->
 
@@ -237,6 +237,7 @@ Samples are in the [`samples/`](https://github.com/googleapis/java-storage/tree/
 | Create Bucket | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/CreateBucket.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/CreateBucket.java) |
 | Create Bucket Dual Region | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/CreateBucketDualRegion.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/CreateBucketDualRegion.java) |
 | Create Bucket Pub Sub Notification | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/CreateBucketPubSubNotification.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/CreateBucketPubSubNotification.java) |
+| Create Bucket With Object Retention | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/CreateBucketWithObjectRetention.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/CreateBucketWithObjectRetention.java) |
 | Create Bucket With Storage Class And Location | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/CreateBucketWithStorageClassAndLocation.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/CreateBucketWithStorageClassAndLocation.java) |
 | Create Bucket With Turbo Replication | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/CreateBucketWithTurboReplication.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/CreateBucketWithTurboReplication.java) |
 | Delete Bucket | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/DeleteBucket.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/DeleteBucket.java) |
@@ -322,6 +323,7 @@ Samples are in the [`samples/`](https://github.com/googleapis/java-storage/tree/
 | Rotate Object Encryption Key | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/object/RotateObjectEncryptionKey.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/object/RotateObjectEncryptionKey.java) |
 | Set Event Based Hold | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/object/SetEventBasedHold.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/object/SetEventBasedHold.java) |
 | Set Object Metadata | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/object/SetObjectMetadata.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/object/SetObjectMetadata.java) |
+| Set Object Retention Policy | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/object/SetObjectRetentionPolicy.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/object/SetObjectRetentionPolicy.java) |
 | Set Temporary Hold | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/object/SetTemporaryHold.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/object/SetTemporaryHold.java) |
 | Stream Object Download | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/object/StreamObjectDownload.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/object/StreamObjectDownload.java) |
 | Stream Object Upload | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/object/StreamObjectUpload.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/object/StreamObjectUpload.java) |
@@ -428,7 +430,7 @@ Java is a registered trademark of Oracle and/or its affiliates.
 [kokoro-badge-link-5]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-storage/java11.html
 [stability-image]: https://img.shields.io/badge/stability-stable-green
 [maven-version-image]: https://img.shields.io/maven-central/v/com.google.cloud/google-cloud-storage.svg
-[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-storage/2.33.0
+[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-storage/2.34.0
 [authentication]: https://github.com/googleapis/google-cloud-java#authentication
 [auth-scopes]: https://developers.google.com/identity/protocols/oauth2/scopes
 [predefined-iam-roles]: https://cloud.google.com/iam/docs/understanding-roles#predefined_roles
diff --git a/gapic-google-cloud-storage-v2/src/main/java/com/google/storage/v2/StorageClient.java b/gapic-google-cloud-storage-v2/src/main/java/com/google/storage/v2/StorageClient.java
index 4034eb12..83ed6ef7 100644
--- a/gapic-google-cloud-storage-v2/src/main/java/com/google/storage/v2/StorageClient.java
+++ b/gapic-google-cloud-storage-v2/src/main/java/com/google/storage/v2/StorageClient.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/gapic-google-cloud-storage-v2/src/main/java/com/google/storage/v2/StorageSettings.java b/gapic-google-cloud-storage-v2/src/main/java/com/google/storage/v2/StorageSettings.java
index 9a8b62b7..8c3a2fdd 100644
--- a/gapic-google-cloud-storage-v2/src/main/java/com/google/storage/v2/StorageSettings.java
+++ b/gapic-google-cloud-storage-v2/src/main/java/com/google/storage/v2/StorageSettings.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -22,7 +22,6 @@ import static com.google.storage.v2.StorageClient.ListNotificationConfigsPagedRe
 import static com.google.storage.v2.StorageClient.ListObjectsPagedResponse;
 
 import com.google.api.core.ApiFunction;
-import com.google.api.core.BetaApi;
 import com.google.api.gax.core.GoogleCredentialsProvider;
 import com.google.api.gax.core.InstantiatingExecutorProvider;
 import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider;
@@ -293,7 +292,6 @@ public class StorageSettings extends ClientSettings<StorageSettings> {
     return StorageStubSettings.defaultTransportChannelProvider();
   }
 
-  @BetaApi("The surface for customizing headers is not stable yet and may change in the future.")
   public static ApiClientHeaderProvider.Builder defaultApiClientHeaderProviderBuilder() {
     return StorageStubSettings.defaultApiClientHeaderProviderBuilder();
   }
diff --git a/gapic-google-cloud-storage-v2/src/main/java/com/google/storage/v2/package-info.java b/gapic-google-cloud-storage-v2/src/main/java/com/google/storage/v2/package-info.java
index 0fc96216..0b7d686c 100644
--- a/gapic-google-cloud-storage-v2/src/main/java/com/google/storage/v2/package-info.java
+++ b/gapic-google-cloud-storage-v2/src/main/java/com/google/storage/v2/package-info.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/gapic-google-cloud-storage-v2/src/main/java/com/google/storage/v2/stub/GrpcStorageCallableFactory.java b/gapic-google-cloud-storage-v2/src/main/java/com/google/storage/v2/stub/GrpcStorageCallableFactory.java
index 4f7f2864..553b1d50 100644
--- a/gapic-google-cloud-storage-v2/src/main/java/com/google/storage/v2/stub/GrpcStorageCallableFactory.java
+++ b/gapic-google-cloud-storage-v2/src/main/java/com/google/storage/v2/stub/GrpcStorageCallableFactory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/gapic-google-cloud-storage-v2/src/main/java/com/google/storage/v2/stub/GrpcStorageStub.java b/gapic-google-cloud-storage-v2/src/main/java/com/google/storage/v2/stub/GrpcStorageStub.java
index 0d32d339..1fe477aa 100644
--- a/gapic-google-cloud-storage-v2/src/main/java/com/google/storage/v2/stub/GrpcStorageStub.java
+++ b/gapic-google-cloud-storage-v2/src/main/java/com/google/storage/v2/stub/GrpcStorageStub.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/gapic-google-cloud-storage-v2/src/main/java/com/google/storage/v2/stub/StorageStub.java b/gapic-google-cloud-storage-v2/src/main/java/com/google/storage/v2/stub/StorageStub.java
index 5476d4aa..81048896 100644
--- a/gapic-google-cloud-storage-v2/src/main/java/com/google/storage/v2/stub/StorageStub.java
+++ b/gapic-google-cloud-storage-v2/src/main/java/com/google/storage/v2/stub/StorageStub.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/gapic-google-cloud-storage-v2/src/main/java/com/google/storage/v2/stub/StorageStubSettings.java b/gapic-google-cloud-storage-v2/src/main/java/com/google/storage/v2/stub/StorageStubSettings.java
index a1b9f34d..e4787eb0 100644
--- a/gapic-google-cloud-storage-v2/src/main/java/com/google/storage/v2/stub/StorageStubSettings.java
+++ b/gapic-google-cloud-storage-v2/src/main/java/com/google/storage/v2/stub/StorageStubSettings.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -23,7 +23,6 @@ import static com.google.storage.v2.StorageClient.ListObjectsPagedResponse;
 
 import com.google.api.core.ApiFunction;
 import com.google.api.core.ApiFuture;
-import com.google.api.core.BetaApi;
 import com.google.api.gax.core.GaxProperties;
 import com.google.api.gax.core.GoogleCredentialsProvider;
 import com.google.api.gax.core.InstantiatingExecutorProvider;
@@ -678,7 +677,6 @@ public class StorageStubSettings extends StubSettings<StorageStubSettings> {
     return defaultGrpcTransportProviderBuilder().build();
   }
 
-  @BetaApi("The surface for customizing headers is not stable yet and may change in the future.")
   public static ApiClientHeaderProvider.Builder defaultApiClientHeaderProviderBuilder() {
     return ApiClientHeaderProvider.newBuilder()
         .setGeneratedLibToken("gapic", GaxProperties.getLibraryVersion(StorageStubSettings.class))
diff --git a/google-cloud-storage-control/src/main/java/com/google/storage/control/v2/StorageControlClient.java b/google-cloud-storage-control/src/main/java/com/google/storage/control/v2/StorageControlClient.java
index 10ac2d09..5a782fb2 100644
--- a/google-cloud-storage-control/src/main/java/com/google/storage/control/v2/StorageControlClient.java
+++ b/google-cloud-storage-control/src/main/java/com/google/storage/control/v2/StorageControlClient.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/google-cloud-storage-control/src/main/java/com/google/storage/control/v2/StorageControlSettings.java b/google-cloud-storage-control/src/main/java/com/google/storage/control/v2/StorageControlSettings.java
index fc29f030..c7f37fa4 100644
--- a/google-cloud-storage-control/src/main/java/com/google/storage/control/v2/StorageControlSettings.java
+++ b/google-cloud-storage-control/src/main/java/com/google/storage/control/v2/StorageControlSettings.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -19,7 +19,6 @@ package com.google.storage.control.v2;
 import static com.google.storage.control.v2.StorageControlClient.ListFoldersPagedResponse;
 
 import com.google.api.core.ApiFunction;
-import com.google.api.core.BetaApi;
 import com.google.api.gax.core.GoogleCredentialsProvider;
 import com.google.api.gax.core.InstantiatingExecutorProvider;
 import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider;
@@ -148,7 +147,6 @@ public class StorageControlSettings extends ClientSettings<StorageControlSetting
     return StorageControlStubSettings.defaultTransportChannelProvider();
   }
 
-  @BetaApi("The surface for customizing headers is not stable yet and may change in the future.")
   public static ApiClientHeaderProvider.Builder defaultApiClientHeaderProviderBuilder() {
     return StorageControlStubSettings.defaultApiClientHeaderProviderBuilder();
   }
diff --git a/google-cloud-storage-control/src/main/java/com/google/storage/control/v2/package-info.java b/google-cloud-storage-control/src/main/java/com/google/storage/control/v2/package-info.java
index e40eb295..126dd95d 100644
--- a/google-cloud-storage-control/src/main/java/com/google/storage/control/v2/package-info.java
+++ b/google-cloud-storage-control/src/main/java/com/google/storage/control/v2/package-info.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/google-cloud-storage-control/src/main/java/com/google/storage/control/v2/stub/GrpcStorageControlCallableFactory.java b/google-cloud-storage-control/src/main/java/com/google/storage/control/v2/stub/GrpcStorageControlCallableFactory.java
index 24311e75..a2dc5b44 100644
--- a/google-cloud-storage-control/src/main/java/com/google/storage/control/v2/stub/GrpcStorageControlCallableFactory.java
+++ b/google-cloud-storage-control/src/main/java/com/google/storage/control/v2/stub/GrpcStorageControlCallableFactory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/google-cloud-storage-control/src/main/java/com/google/storage/control/v2/stub/GrpcStorageControlStub.java b/google-cloud-storage-control/src/main/java/com/google/storage/control/v2/stub/GrpcStorageControlStub.java
index 6e630f3b..3bd55270 100644
--- a/google-cloud-storage-control/src/main/java/com/google/storage/control/v2/stub/GrpcStorageControlStub.java
+++ b/google-cloud-storage-control/src/main/java/com/google/storage/control/v2/stub/GrpcStorageControlStub.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/google-cloud-storage-control/src/main/java/com/google/storage/control/v2/stub/StorageControlStub.java b/google-cloud-storage-control/src/main/java/com/google/storage/control/v2/stub/StorageControlStub.java
index 19bee2f6..681466af 100644
--- a/google-cloud-storage-control/src/main/java/com/google/storage/control/v2/stub/StorageControlStub.java
+++ b/google-cloud-storage-control/src/main/java/com/google/storage/control/v2/stub/StorageControlStub.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/google-cloud-storage-control/src/main/java/com/google/storage/control/v2/stub/StorageControlStubSettings.java b/google-cloud-storage-control/src/main/java/com/google/storage/control/v2/stub/StorageControlStubSettings.java
index 225b5684..c65cddfc 100644
--- a/google-cloud-storage-control/src/main/java/com/google/storage/control/v2/stub/StorageControlStubSettings.java
+++ b/google-cloud-storage-control/src/main/java/com/google/storage/control/v2/stub/StorageControlStubSettings.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -277,7 +277,6 @@ public class StorageControlStubSettings extends StubSettings<StorageControlStubS
     return defaultGrpcTransportProviderBuilder().build();
   }
 
-  @BetaApi("The surface for customizing headers is not stable yet and may change in the future.")
   public static ApiClientHeaderProvider.Builder defaultApiClientHeaderProviderBuilder() {
     return ApiClientHeaderProvider.newBuilder()
         .setGeneratedLibToken(
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BidiBlobWriteSessionConfig.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BidiBlobWriteSessionConfig.java
new file mode 100644
index 00000000..06dd7e7a
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BidiBlobWriteSessionConfig.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2024 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 com.google.api.core.ApiFuture;
+import com.google.api.core.ApiFutures;
+import com.google.api.core.BetaApi;
+import com.google.api.core.InternalApi;
+import com.google.api.gax.grpc.GrpcCallContext;
+import com.google.common.base.Preconditions;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.storage.v2.BidiWriteObjectRequest;
+import com.google.storage.v2.BidiWriteObjectResponse;
+import java.io.IOException;
+import java.nio.channels.WritableByteChannel;
+import java.time.Clock;
+
+public class BidiBlobWriteSessionConfig extends BlobWriteSessionConfig
+    implements BlobWriteSessionConfig.GrpcCompatible {
+  private static final long serialVersionUID = -903533790705476197L;
+
+  private final int bufferSize;
+
+  @InternalApi
+  BidiBlobWriteSessionConfig(int bufferSize) {
+    this.bufferSize = bufferSize;
+  }
+
+  /**
+   * The number of bytes to hold in the buffer before each flush
+   *
+   * <p><i>Default:</i> {@code 16777216 (16 MiB)}
+   *
+   * @see #withBufferSize(int)
+   * @since 2.34.0 This new api is in preview and is subject to breaking changes.
+   */
+  public int getBufferSize() {
+    return bufferSize;
+  }
+
+  @Override
+  WriterFactory createFactory(Clock clock) throws IOException {
+    return new Factory(ByteSizeConstants._16MiB);
+  }
+
+  @InternalApi
+  private static final class Factory implements WriterFactory {
+    private static final Conversions.Decoder<BidiWriteObjectResponse, BlobInfo>
+        WRITE_OBJECT_RESPONSE_BLOB_INFO_DECODER =
+            Conversions.grpc().blobInfo().compose(BidiWriteObjectResponse::getResource);
+
+    private final int bufferSize;
+
+    private Factory(int bufferSize) {
+      this.bufferSize = bufferSize;
+    }
+
+    @InternalApi
+    @Override
+    public WritableByteChannelSession<?, BlobInfo> writeSession(
+        StorageInternal s, BlobInfo info, UnifiedOpts.Opts<UnifiedOpts.ObjectTargetOpt> opts) {
+      if (s instanceof GrpcStorageImpl) {
+        return new DecoratedWritableByteChannelSession<>(
+            new LazySession<>(
+                new LazyWriteChannel<>(
+                    () -> {
+                      GrpcStorageImpl grpc = (GrpcStorageImpl) s;
+                      GrpcCallContext grpcCallContext =
+                          opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault());
+                      BidiWriteObjectRequest req = grpc.getBidiWriteObjectRequest(info, opts);
+
+                      ApiFuture<BidiResumableWrite> startResumableWrite =
+                          grpc.startResumableWrite(grpcCallContext, req);
+                      return ResumableMedia.gapic()
+                          .write()
+                          .bidiByteChannel(grpc.storageClient.bidiWriteObjectCallable())
+                          .setHasher(Hasher.noop())
+                          .setByteStringStrategy(ByteStringStrategy.copy())
+                          .resumable()
+                          .withRetryConfig(
+                              grpc.getOptions(), grpc.retryAlgorithmManager.idempotent())
+                          .buffered(BufferHandle.allocate(bufferSize))
+                          .setStartAsync(startResumableWrite)
+                          .build();
+                    })),
+            WRITE_OBJECT_RESPONSE_BLOB_INFO_DECODER);
+      } else {
+        throw new IllegalStateException(
+            "Unknown Storage implementation: " + s.getClass().getName());
+      }
+    }
+  }
+
+  /**
+   * Create a new instance with the {@code bufferSize} set to the specified value.
+   *
+   * <p><i>Default:</i> {@code 16777216 (16 MiB)}
+   *
+   * @param bufferSize The number of bytes to hold in the buffer before each flush. Must be &gt;=
+   *     {@code 262144 (256 KiB)}
+   * @return The new instance
+   * @see #getBufferSize()
+   * @since 2.34.0 This new api is in preview and is subject to breaking changes.
+   */
+  @BetaApi
+  public BidiBlobWriteSessionConfig withBufferSize(int bufferSize) {
+    Preconditions.checkArgument(
+        bufferSize >= ByteSizeConstants._256KiB,
+        "bufferSize must be >= %d",
+        ByteSizeConstants._256KiB);
+    return new BidiBlobWriteSessionConfig(bufferSize);
+  }
+
+  private static final class DecoratedWritableByteChannelSession<WBC extends WritableByteChannel, T>
+      implements WritableByteChannelSession<WBC, BlobInfo> {
+
+    private final WritableByteChannelSession<WBC, T> delegate;
+    private final Conversions.Decoder<T, BlobInfo> decoder;
+
+    private DecoratedWritableByteChannelSession(
+        WritableByteChannelSession<WBC, T> delegate, Conversions.Decoder<T, BlobInfo> decoder) {
+      this.delegate = delegate;
+      this.decoder = decoder;
+    }
+
+    @Override
+    public WBC open() {
+      try {
+        return WritableByteChannelSession.super.open();
+      } catch (Exception e) {
+        throw StorageException.coalesce(e);
+      }
+    }
+
+    @Override
+    public ApiFuture<WBC> openAsync() {
+      return delegate.openAsync();
+    }
+
+    @Override
+    public ApiFuture<BlobInfo> getResult() {
+      return ApiFutures.transform(
+          delegate.getResult(), decoder::decode, MoreExecutors.directExecutor());
+    }
+  }
+
+  private static final class LazySession<R>
+      implements WritableByteChannelSession<
+          BufferedWritableByteChannelSession.BufferedWritableByteChannel, R> {
+    private final LazyWriteChannel<R> lazy;
+
+    private LazySession(LazyWriteChannel<R> lazy) {
+      this.lazy = lazy;
+    }
+
+    @Override
+    public ApiFuture<BufferedWritableByteChannelSession.BufferedWritableByteChannel> openAsync() {
+      return lazy.getSession().openAsync();
+    }
+
+    @Override
+    public ApiFuture<R> getResult() {
+      return lazy.getSession().getResult();
+    }
+  }
+}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BidiResumableWrite.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BidiResumableWrite.java
new file mode 100644
index 00000000..dab7b247
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BidiResumableWrite.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2023 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 com.google.cloud.storage.StorageV2ProtoUtils.fmtProto;
+
+import com.google.cloud.storage.BidiWriteCtx.BidiWriteObjectRequestBuilderFactory;
+import com.google.storage.v2.BidiWriteObjectRequest;
+import com.google.storage.v2.StartResumableWriteRequest;
+import com.google.storage.v2.StartResumableWriteResponse;
+import java.util.Objects;
+import java.util.function.Function;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+final class BidiResumableWrite implements BidiWriteObjectRequestBuilderFactory {
+
+  private final StartResumableWriteRequest req;
+  private final StartResumableWriteResponse res;
+
+  private final BidiWriteObjectRequest writeRequest;
+
+  public BidiResumableWrite(
+      StartResumableWriteRequest req,
+      StartResumableWriteResponse res,
+      Function<String, BidiWriteObjectRequest> f) {
+    this.req = req;
+    this.res = res;
+    this.writeRequest = f.apply(res.getUploadId());
+  }
+
+  public StartResumableWriteRequest getReq() {
+    return req;
+  }
+
+  public StartResumableWriteResponse getRes() {
+    return res;
+  }
+
+  @Override
+  public BidiWriteObjectRequest.Builder newBuilder() {
+    return writeRequest.toBuilder();
+  }
+
+  @Override
+  public @Nullable String bucketName() {
+    if (req.hasWriteObjectSpec() && req.getWriteObjectSpec().hasResource()) {
+      return req.getWriteObjectSpec().getResource().getBucket();
+    }
+    return null;
+  }
+
+  @Override
+  public String toString() {
+    return "BidiResumableWrite{" + "req=" + fmtProto(req) + ", res=" + fmtProto(res) + '}';
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof ResumableWrite)) {
+      return false;
+    }
+    ResumableWrite resumableWrite = (ResumableWrite) o;
+    return Objects.equals(req, resumableWrite.getReq())
+        && Objects.equals(res, resumableWrite.getRes());
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(req, res);
+  }
+
+  /**
+   * Helper function which is more specific than {@link Function#identity()}. Constraining the input
+   * and output to be exactly {@link BidiResumableWrite}.
+   */
+  static BidiResumableWrite identity(BidiResumableWrite w) {
+    return w;
+  }
+}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BidiWriteCtx.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BidiWriteCtx.java
new file mode 100644
index 00000000..09e4177c
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BidiWriteCtx.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2023 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 com.google.cloud.storage.StorageV2ProtoUtils.fmtProto;
+
+import com.google.cloud.storage.BidiWriteCtx.BidiWriteObjectRequestBuilderFactory;
+import com.google.cloud.storage.Crc32cValue.Crc32cLengthKnown;
+import com.google.storage.v2.BidiWriteObjectRequest;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+final class BidiWriteCtx<RequestFactoryT extends BidiWriteObjectRequestBuilderFactory> {
+
+  private final RequestFactoryT requestFactory;
+
+  private final AtomicLong totalSentBytes;
+  private final AtomicLong confirmedBytes;
+  private final AtomicReference<Crc32cLengthKnown> cumulativeCrc32c;
+
+  BidiWriteCtx(RequestFactoryT requestFactory) {
+    this.requestFactory = requestFactory;
+    this.totalSentBytes = new AtomicLong(0);
+    this.confirmedBytes = new AtomicLong(0);
+    this.cumulativeCrc32c = new AtomicReference<>();
+  }
+
+  public RequestFactoryT getRequestFactory() {
+    return requestFactory;
+  }
+
+  public BidiWriteObjectRequest.Builder newRequestBuilder() {
+    return requestFactory.newBuilder();
+  }
+
+  public AtomicLong getTotalSentBytes() {
+    return totalSentBytes;
+  }
+
+  public AtomicLong getConfirmedBytes() {
+    return confirmedBytes;
+  }
+
+  public AtomicReference<Crc32cLengthKnown> getCumulativeCrc32c() {
+    return cumulativeCrc32c;
+  }
+
+  // TODO: flush this out more
+  boolean isDirty() {
+    return confirmedBytes.get() == totalSentBytes.get();
+  }
+
+  @Override
+  public String toString() {
+    return "ServerState{"
+        + "requestFactory="
+        + requestFactory
+        + ", totalSentBytes="
+        + totalSentBytes
+        + ", confirmedBytes="
+        + confirmedBytes
+        + ", totalSentCrc32c="
+        + cumulativeCrc32c
+        + '}';
+  }
+
+  interface BidiWriteObjectRequestBuilderFactory {
+    BidiWriteObjectRequest.Builder newBuilder();
+
+    @Nullable
+    String bucketName();
+
+    static BidiSimpleWriteObjectRequestBuilderFactory simple(BidiWriteObjectRequest req) {
+      return new BidiSimpleWriteObjectRequestBuilderFactory(req);
+    }
+  }
+
+  static final class BidiSimpleWriteObjectRequestBuilderFactory
+      implements BidiWriteObjectRequestBuilderFactory {
+    private final BidiWriteObjectRequest req;
+
+    private BidiSimpleWriteObjectRequestBuilderFactory(BidiWriteObjectRequest req) {
+      this.req = req;
+    }
+
+    @Override
+    public BidiWriteObjectRequest.Builder newBuilder() {
+      return req.toBuilder();
+    }
+
+    @Override
+    public @Nullable String bucketName() {
+      if (req.hasWriteObjectSpec() && req.getWriteObjectSpec().hasResource()) {
+        return req.getWriteObjectSpec().getResource().getBucket();
+      }
+      return null;
+    }
+
+    @Override
+    public String toString() {
+      return "SimpleBidiWriteObjectRequestBuilderFactory{" + "req=" + fmtProto(req) + '}';
+    }
+  }
+}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfigs.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfigs.java
index 0f11cb19..43b68d8a 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfigs.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfigs.java
@@ -246,6 +246,21 @@ public final class BlobWriteSessionConfigs {
     return new DefaultBlobWriteSessionConfig(ByteSizeConstants._16MiB);
   }
 
+  /**
+   * Factory to produce a resumable upload using a bi-directional stream. This should provide a
+   * small performance increase compared to a regular resumable upload.
+   *
+   * <p>Configuration of the buffer size can be performed via {@link
+   * BidiBlobWriteSessionConfig#withBufferSize(int)}.
+   *
+   * @since 2.34.0 This new api is in preview and is subject to breaking changes.
+   */
+  @BetaApi
+  @TransportCompatibility({Transport.GRPC})
+  public static BidiBlobWriteSessionConfig bidiWrite() {
+    return new BidiBlobWriteSessionConfig(ByteSizeConstants._16MiB);
+  }
+
   /**
    * Create a new {@link BlobWriteSessionConfig} which will first buffer the content of the object
    * to a temporary file under {@code java.io.tmpdir}.
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicBidiUnbufferedWritableByteChannel.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicBidiUnbufferedWritableByteChannel.java
new file mode 100644
index 00000000..d19a8802
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicBidiUnbufferedWritableByteChannel.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2023 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 com.google.api.core.SettableApiFuture;
+import com.google.cloud.storage.ChunkSegmenter.ChunkSegment;
+import com.google.cloud.storage.Crc32cValue.Crc32cLengthKnown;
+import com.google.cloud.storage.UnbufferedWritableByteChannelSession.UnbufferedWritableByteChannel;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.protobuf.ByteString;
+import com.google.storage.v2.BidiWriteObjectRequest;
+import com.google.storage.v2.BidiWriteObjectResponse;
+import com.google.storage.v2.ChecksummedData;
+import com.google.storage.v2.ObjectChecksums;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ClosedChannelException;
+import java.util.ArrayList;
+import java.util.List;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+final class GapicBidiUnbufferedWritableByteChannel<
+        RequestFactoryT extends BidiWriteCtx.BidiWriteObjectRequestBuilderFactory>
+    implements UnbufferedWritableByteChannel {
+
+  private final SettableApiFuture<BidiWriteObjectResponse> resultFuture;
+  private final ChunkSegmenter chunkSegmenter;
+
+  private final BidiWriteCtx<RequestFactoryT> writeCtx;
+  private final WriteFlushStrategy.BidiFlusher flusher;
+
+  private boolean open = true;
+  private boolean finished = false;
+
+  GapicBidiUnbufferedWritableByteChannel(
+      SettableApiFuture<BidiWriteObjectResponse> resultFuture,
+      ChunkSegmenter chunkSegmenter,
+      RequestFactoryT requestFactory,
+      WriteFlushStrategy.BidiFlusherFactory flusherFactory) {
+    this.resultFuture = resultFuture;
+    this.chunkSegmenter = chunkSegmenter;
+
+    this.writeCtx = new BidiWriteCtx<>(requestFactory);
+    this.flusher =
+        flusherFactory.newFlusher(
+            requestFactory.bucketName(), writeCtx.getConfirmedBytes()::set, resultFuture::set);
+  }
+
+  @Override
+  public long write(ByteBuffer[] srcs, int srcsOffset, int srcsLength) throws IOException {
+    return internalWrite(srcs, srcsOffset, srcsLength, false);
+  }
+
+  @Override
+  public boolean isOpen() {
+    return open;
+  }
+
+  @Override
+  public void close() throws IOException {
+    if (!finished) {
+      BidiWriteObjectRequest message = finishMessage();
+      try {
+        flusher.close(message);
+        finished = true;
+      } catch (RuntimeException e) {
+        resultFuture.setException(e);
+        throw e;
+      }
+    } else {
+      flusher.close(null);
+    }
+    open = false;
+  }
+
+  @VisibleForTesting
+  BidiWriteCtx<RequestFactoryT> getWriteCtx() {
+    return writeCtx;
+  }
+
+  private long internalWrite(ByteBuffer[] srcs, int srcsOffset, int srcsLength, boolean finalize)
+      throws ClosedChannelException {
+    if (!open) {
+      throw new ClosedChannelException();
+    }
+
+    ChunkSegment[] data = chunkSegmenter.segmentBuffers(srcs, srcsOffset, srcsLength);
+
+    List<BidiWriteObjectRequest> messages = new ArrayList<>();
+
+    int bytesConsumed = 0;
+    for (ChunkSegment datum : data) {
+      Crc32cLengthKnown crc32c = datum.getCrc32c();
+      ByteString b = datum.getB();
+      int contentSize = b.size();
+      long offset = writeCtx.getTotalSentBytes().getAndAdd(contentSize);
+      Crc32cLengthKnown cumulative =
+          writeCtx
+              .getCumulativeCrc32c()
+              .accumulateAndGet(crc32c, chunkSegmenter.getHasher()::nullSafeConcat);
+      ChecksummedData.Builder checksummedData = ChecksummedData.newBuilder().setContent(b);
+      if (crc32c != null) {
+        checksummedData.setCrc32C(crc32c.getValue());
+      }
+      BidiWriteObjectRequest.Builder builder =
+          writeCtx
+              .newRequestBuilder()
+              .setWriteOffset(offset)
+              .setChecksummedData(checksummedData.build());
+      if (!datum.isOnlyFullBlocks()) {
+        builder.setFinishWrite(true);
+        if (cumulative != null) {
+          builder.setObjectChecksums(
+              ObjectChecksums.newBuilder().setCrc32C(cumulative.getValue()).build());
+        }
+        finished = true;
+      }
+
+      BidiWriteObjectRequest build = builder.build();
+      messages.add(build);
+      bytesConsumed += contentSize;
+    }
+    if (finalize && !finished) {
+      messages.add(finishMessage());
+      finished = true;
+    }
+
+    try {
+      flusher.flush(messages);
+    } catch (RuntimeException e) {
+      resultFuture.setException(e);
+      throw e;
+    }
+
+    return bytesConsumed;
+  }
+
+  @NonNull
+  private BidiWriteObjectRequest finishMessage() {
+    long offset = writeCtx.getTotalSentBytes().get();
+    Crc32cLengthKnown crc32cValue = writeCtx.getCumulativeCrc32c().get();
+
+    BidiWriteObjectRequest.Builder b =
+        writeCtx.newRequestBuilder().setFinishWrite(true).setWriteOffset(offset);
+    if (crc32cValue != null) {
+      b.setObjectChecksums(ObjectChecksums.newBuilder().setCrc32C(crc32cValue.getValue()).build());
+    }
+    BidiWriteObjectRequest message = b.build();
+    return message;
+  }
+}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicBidiWritableByteChannelSessionBuilder.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicBidiWritableByteChannelSessionBuilder.java
new file mode 100644
index 00000000..e26d33bb
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicBidiWritableByteChannelSessionBuilder.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2023 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 java.util.Objects.requireNonNull;
+
+import com.google.api.core.ApiFuture;
+import com.google.api.core.SettableApiFuture;
+import com.google.api.gax.retrying.ResultRetryAlgorithm;
+import com.google.api.gax.rpc.BidiStreamingCallable;
+import com.google.cloud.storage.ChannelSession.BufferedWriteSession;
+import com.google.cloud.storage.Retrying.RetryingDependencies;
+import com.google.cloud.storage.UnbufferedWritableByteChannelSession.UnbufferedWritableByteChannel;
+import com.google.storage.v2.BidiWriteObjectRequest;
+import com.google.storage.v2.BidiWriteObjectResponse;
+import com.google.storage.v2.ServiceConstants.Values;
+import java.nio.ByteBuffer;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+final class GapicBidiWritableByteChannelSessionBuilder {
+
+  private final BidiStreamingCallable<BidiWriteObjectRequest, BidiWriteObjectResponse> write;
+  private Hasher hasher;
+  private ByteStringStrategy byteStringStrategy;
+
+  GapicBidiWritableByteChannelSessionBuilder(
+      BidiStreamingCallable<BidiWriteObjectRequest, BidiWriteObjectResponse> write) {
+    this.write = write;
+    this.hasher = Hasher.noop();
+    this.byteStringStrategy = ByteStringStrategy.copy();
+  }
+
+  /**
+   * Set the {@link Hasher} to apply to the bytes passing through the built session's channel.
+   *
+   * <p>Default: {@link Hasher#noop()}
+   *
+   * @see Hasher#enabled()
+   * @see Hasher#noop()
+   */
+  GapicBidiWritableByteChannelSessionBuilder setHasher(Hasher hasher) {
+    this.hasher = requireNonNull(hasher, "hasher must be non null");
+    return this;
+  }
+
+  /**
+   * Set the {@link ByteStringStrategy} to be used when constructing {@link
+   * com.google.protobuf.ByteString ByteString}s from {@link ByteBuffer}s.
+   *
+   * <p>Default: {@link ByteStringStrategy#copy()}
+   *
+   * <p>Note: usage of {@link ByteStringStrategy#noCopy()} requires that any {@link ByteBuffer}
+   * passed to the session's channel not be modified while {@link
+   * java.nio.channels.WritableByteChannel#write(ByteBuffer)} is processing.
+   *
+   * @see ByteStringStrategy#copy()
+   * @see ByteStringStrategy#noCopy()
+   */
+  GapicBidiWritableByteChannelSessionBuilder setByteStringStrategy(
+      ByteStringStrategy byteStringStrategy) {
+    this.byteStringStrategy =
+        requireNonNull(byteStringStrategy, "byteStringStrategy must be non null");
+    return this;
+  }
+
+  /**
+   * When constructing a bidi channel session, there is always a {@link
+   * GapicBidiUnbufferedWritableByteChannel} at the bottom of it. This method creates a BiFunction
+   * which will instantiate the {@link GapicBidiUnbufferedWritableByteChannel} when provided with a
+   * {@code StartT} value and a {@code SettableApiFuture<BidiWriteObjectResponse>}.
+   *
+   * <p>As part of providing the function, the provided parameters {@code BidiFlusherFactory} and
+   * {@code f} are "bound" into the returned function. In conjunction with the configured fields of
+   * this class a new instance of {@link GapicBidiUnbufferedWritableByteChannel} can be constructed.
+   */
+  private <StartT, RequestFactoryT extends BidiWriteCtx.BidiWriteObjectRequestBuilderFactory>
+      BiFunction<StartT, SettableApiFuture<BidiWriteObjectResponse>, UnbufferedWritableByteChannel>
+          bindFunction(
+              WriteFlushStrategy.BidiFlusherFactory flusherFactory,
+              Function<StartT, RequestFactoryT> f) {
+    // it is theoretically possible that the setter methods for the following variables could
+    // be called again between when this method is invoked and the resulting function is invoked.
+    // To ensure we are using the specified values at the point in time they are bound to the
+    // function read them into local variables which will be closed over rather than the class
+    // fields.
+    ByteStringStrategy boundStrategy = byteStringStrategy;
+    Hasher boundHasher = hasher;
+    return (start, resultFuture) ->
+        new GapicBidiUnbufferedWritableByteChannel<>(
+            resultFuture,
+            new ChunkSegmenter(boundHasher, boundStrategy, Values.MAX_WRITE_CHUNK_BYTES_VALUE),
+            f.apply(start),
+            flusherFactory);
+  }
+
+  GapicBidiWritableByteChannelSessionBuilder.ResumableUploadBuilder resumable() {
+    return new GapicBidiWritableByteChannelSessionBuilder.ResumableUploadBuilder();
+  }
+
+  final class ResumableUploadBuilder {
+
+    private RetryingDependencies deps;
+    private ResultRetryAlgorithm<?> alg;
+
+    ResumableUploadBuilder() {
+      this.deps = RetryingDependencies.attemptOnce();
+      this.alg = Retrying.neverRetry();
+    }
+
+    ResumableUploadBuilder withRetryConfig(RetryingDependencies deps, ResultRetryAlgorithm<?> alg) {
+      this.deps = requireNonNull(deps, "deps must be non null");
+      this.alg = requireNonNull(alg, "alg must be non null");
+      return this;
+    }
+
+    /**
+     * Buffer using {@code byteBuffer} worth of space before attempting to flush.
+     *
+     * <p>The provided {@link ByteBuffer} <i>should</i> be aligned with GCSs block size of <a
+     * target="_blank"
+     * href="https://cloud.google.com/storage/docs/performing-resumable-uploads#chunked-upload">256
+     * KiB</a>.
+     */
+    BufferedResumableUploadBuilder buffered(ByteBuffer byteBuffer) {
+      return buffered(BufferHandle.handleOf(byteBuffer));
+    }
+
+    BufferedResumableUploadBuilder buffered(BufferHandle bufferHandle) {
+      return new BufferedResumableUploadBuilder(bufferHandle);
+    }
+
+    final class BufferedResumableUploadBuilder {
+
+      private final BufferHandle bufferHandle;
+
+      private ApiFuture<BidiResumableWrite> start;
+
+      BufferedResumableUploadBuilder(BufferHandle bufferHandle) {
+        this.bufferHandle = bufferHandle;
+      }
+
+      /**
+       * Set the Future which will contain the ResumableWrite information necessary to open the
+       * Write stream.
+       */
+      BufferedResumableUploadBuilder setStartAsync(ApiFuture<BidiResumableWrite> start) {
+        this.start = requireNonNull(start, "start must be non null");
+        return this;
+      }
+
+      BufferedWritableByteChannelSession<BidiWriteObjectResponse> build() {
+        return new BufferedWriteSession<>(
+            requireNonNull(start, "start must be non null"),
+            bindFunction(
+                    WriteFlushStrategy.defaultBidiFlusher(
+                        write, deps, alg, Retrying::newCallContext),
+                    BidiResumableWrite::identity)
+                .andThen(c -> new DefaultBufferedWritableByteChannel(bufferHandle, c))
+                .andThen(StorageByteChannels.writable()::createSynchronized));
+      }
+    }
+  }
+}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUploadSessionBuilder.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUploadSessionBuilder.java
index 7b4c6a94..ac520213 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUploadSessionBuilder.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUploadSessionBuilder.java
@@ -18,9 +18,12 @@ package com.google.cloud.storage;
 
 import com.google.api.core.ApiFuture;
 import com.google.api.core.ApiFutures;
+import com.google.api.gax.rpc.BidiStreamingCallable;
 import com.google.api.gax.rpc.ClientStreamingCallable;
 import com.google.api.gax.rpc.UnaryCallable;
 import com.google.common.util.concurrent.MoreExecutors;
+import com.google.storage.v2.BidiWriteObjectRequest;
+import com.google.storage.v2.BidiWriteObjectResponse;
 import com.google.storage.v2.StartResumableWriteRequest;
 import com.google.storage.v2.StartResumableWriteResponse;
 import com.google.storage.v2.WriteObjectRequest;
@@ -40,6 +43,11 @@ final class GapicUploadSessionBuilder {
     return new GapicWritableByteChannelSessionBuilder(write);
   }
 
+  GapicBidiWritableByteChannelSessionBuilder bidiByteChannel(
+      BidiStreamingCallable<BidiWriteObjectRequest, BidiWriteObjectResponse> write) {
+    return new GapicBidiWritableByteChannelSessionBuilder(write);
+  }
+
   ApiFuture<ResumableWrite> resumableWrite(
       UnaryCallable<StartResumableWriteRequest, StartResumableWriteResponse> x,
       WriteObjectRequest writeObjectRequest) {
@@ -62,4 +70,27 @@ final class GapicUploadSessionBuilder {
         (resp) -> new ResumableWrite(req, resp, f),
         MoreExecutors.directExecutor());
   }
+
+  ApiFuture<BidiResumableWrite> bidiResumableWrite(
+      UnaryCallable<StartResumableWriteRequest, StartResumableWriteResponse> x,
+      BidiWriteObjectRequest writeObjectRequest) {
+    StartResumableWriteRequest.Builder b = StartResumableWriteRequest.newBuilder();
+    if (writeObjectRequest.hasWriteObjectSpec()) {
+      b.setWriteObjectSpec(writeObjectRequest.getWriteObjectSpec());
+    }
+    if (writeObjectRequest.hasCommonObjectRequestParams()) {
+      b.setCommonObjectRequestParams(writeObjectRequest.getCommonObjectRequestParams());
+    }
+    if (writeObjectRequest.hasObjectChecksums()) {
+      b.setObjectChecksums(writeObjectRequest.getObjectChecksums());
+    }
+    StartResumableWriteRequest req = b.build();
+    Function<String, BidiWriteObjectRequest> f =
+        uploadId ->
+            writeObjectRequest.toBuilder().clearWriteObjectSpec().setUploadId(uploadId).build();
+    return ApiFutures.transform(
+        x.futureCall(req),
+        (resp) -> new BidiResumableWrite(req, resp, f),
+        MoreExecutors.directExecutor());
+  }
 }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcRetryAlgorithmManager.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcRetryAlgorithmManager.java
index 703cd152..ea481ab2 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcRetryAlgorithmManager.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcRetryAlgorithmManager.java
@@ -20,6 +20,7 @@ import com.google.api.gax.retrying.ResultRetryAlgorithm;
 import com.google.iam.v1.GetIamPolicyRequest;
 import com.google.iam.v1.SetIamPolicyRequest;
 import com.google.iam.v1.TestIamPermissionsRequest;
+import com.google.storage.v2.BidiWriteObjectRequest;
 import com.google.storage.v2.ComposeObjectRequest;
 import com.google.storage.v2.CreateBucketRequest;
 import com.google.storage.v2.CreateHmacKeyRequest;
@@ -201,4 +202,10 @@ final class GrpcRetryAlgorithmManager implements Serializable {
         ? retryStrategy.getIdempotentHandler()
         : retryStrategy.getNonidempotentHandler();
   }
+
+  public ResultRetryAlgorithm<?> getFor(BidiWriteObjectRequest req) {
+    return req.getWriteObjectSpec().hasIfGenerationMatch()
+        ? retryStrategy.getIdempotentHandler()
+        : retryStrategy.getNonidempotentHandler();
+  }
 }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java
index 94ed6b33..53c73d4d 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java
@@ -84,6 +84,7 @@ import com.google.iam.v1.SetIamPolicyRequest;
 import com.google.iam.v1.TestIamPermissionsRequest;
 import com.google.protobuf.ByteString;
 import com.google.protobuf.FieldMask;
+import com.google.storage.v2.BidiWriteObjectRequest;
 import com.google.storage.v2.BucketAccessControl;
 import com.google.storage.v2.ComposeObjectRequest;
 import com.google.storage.v2.ComposeObjectRequest.SourceObject;
@@ -1807,8 +1808,30 @@ final class GrpcStorageImpl extends BaseService<StorageOptions>
     return opts.writeObjectRequest().apply(requestBuilder).build();
   }
 
+  BidiWriteObjectRequest getBidiWriteObjectRequest(BlobInfo info, Opts<ObjectTargetOpt> opts) {
+    Object object = codecs.blobInfo().encode(info);
+    Object.Builder objectBuilder =
+        object
+            .toBuilder()
+            // required if the data is changing
+            .clearChecksums()
+            // trimmed to shave payload size
+            .clearGeneration()
+            .clearMetageneration()
+            .clearSize()
+            .clearCreateTime()
+            .clearUpdateTime();
+    WriteObjectSpec.Builder specBuilder = WriteObjectSpec.newBuilder().setResource(objectBuilder);
+
+    BidiWriteObjectRequest.Builder requestBuilder =
+        BidiWriteObjectRequest.newBuilder().setWriteObjectSpec(specBuilder);
+
+    return opts.bidiWriteObjectRequest().apply(requestBuilder).build();
+  }
+
   private UnbufferedReadableByteChannelSession<Object> unbufferedReadSession(
       BlobId blob, BlobSourceOption[] options) {
+
     Opts<ObjectSourceOpt> opts = Opts.unwrap(options).resolveFrom(blob).prepend(defaultOpts);
     ReadObjectRequest readObjectRequest = getReadObjectRequest(blob, opts);
     Set<StatusCode.Code> codes =
@@ -1838,6 +1861,19 @@ final class GrpcStorageImpl extends BaseService<StorageOptions>
             req);
   }
 
+  ApiFuture<BidiResumableWrite> startResumableWrite(
+      GrpcCallContext grpcCallContext, BidiWriteObjectRequest req) {
+    Set<StatusCode.Code> codes = resultRetryAlgorithmToCodes(retryAlgorithmManager.getFor(req));
+    GrpcCallContext merge = Utils.merge(grpcCallContext, Retrying.newCallContext());
+    return ResumableMedia.gapic()
+        .write()
+        .bidiResumableWrite(
+            storageClient
+                .startResumableWriteCallable()
+                .withDefaultCallContext(merge.withRetryableCodes(codes)),
+            req);
+  }
+
   private SourceObject sourceObjectEncode(SourceBlob from) {
     SourceObject.Builder to = SourceObject.newBuilder();
     to.setName(from.getName());
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java
index 18f79be5..dbf7bb64 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java
@@ -37,6 +37,7 @@ import com.google.common.io.BaseEncoding;
 import com.google.iam.v1.GetIamPolicyRequest;
 import com.google.protobuf.ByteString;
 import com.google.protobuf.FieldMask;
+import com.google.storage.v2.BidiWriteObjectRequest;
 import com.google.storage.v2.CommonObjectRequestParams;
 import com.google.storage.v2.ComposeObjectRequest;
 import com.google.storage.v2.CreateBucketRequest;
@@ -171,6 +172,10 @@ final class UnifiedOpts {
       return Mapper.identity();
     }
 
+    default Mapper<BidiWriteObjectRequest.Builder> bidiWriteObject() {
+      return Mapper.identity();
+    }
+
     default Mapper<UpdateObjectRequest.Builder> updateObject() {
       return Mapper.identity();
     }
@@ -2266,6 +2271,10 @@ final class UnifiedOpts {
       return fuseMappers(ObjectTargetOpt.class, ObjectTargetOpt::writeObject);
     }
 
+    Mapper<BidiWriteObjectRequest.Builder> bidiWriteObjectRequest() {
+      return fuseMappers(ObjectTargetOpt.class, ObjectTargetOpt::bidiWriteObject);
+    }
+
     Mapper<GetObjectRequest.Builder> getObjectsRequest() {
       return fuseMappers(ObjectSourceOpt.class, ObjectSourceOpt::getObject);
     }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/WriteFlushStrategy.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/WriteFlushStrategy.java
index 806c2b42..0a191c7d 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/WriteFlushStrategy.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/WriteFlushStrategy.java
@@ -20,11 +20,14 @@ import com.google.api.core.SettableApiFuture;
 import com.google.api.gax.grpc.GrpcCallContext;
 import com.google.api.gax.retrying.ResultRetryAlgorithm;
 import com.google.api.gax.rpc.ApiStreamObserver;
+import com.google.api.gax.rpc.BidiStreamingCallable;
 import com.google.api.gax.rpc.ClientStreamingCallable;
 import com.google.cloud.storage.Conversions.Decoder;
 import com.google.cloud.storage.Retrying.RetryingDependencies;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.storage.v2.BidiWriteObjectRequest;
+import com.google.storage.v2.BidiWriteObjectResponse;
 import com.google.storage.v2.WriteObjectRequest;
 import com.google.storage.v2.WriteObjectResponse;
 import java.util.List;
@@ -72,6 +75,28 @@ final class WriteFlushStrategy {
             baseContextSupplier);
   }
 
+  /**
+   * Create a {@link BidiFlusher} which will keep a bidirectional stream open, flushing and sending
+   * the appropriate signals to GCS when the buffer is full.
+   */
+  static BidiFlusherFactory defaultBidiFlusher(
+      BidiStreamingCallable<BidiWriteObjectRequest, BidiWriteObjectResponse> write,
+      RetryingDependencies deps,
+      ResultRetryAlgorithm<?> alg,
+      Supplier<GrpcCallContext> baseContextSupplier) {
+    return (String bucketName,
+        LongConsumer committedTotalBytesCallback,
+        Consumer<BidiWriteObjectResponse> onSuccessCallback) ->
+        new DefaultBidiFlusher(
+            write,
+            deps,
+            alg,
+            bucketName,
+            committedTotalBytesCallback,
+            onSuccessCallback,
+            baseContextSupplier);
+  }
+
   /**
    * Create a {@link Flusher} which will "fsync" only on {@link Flusher#close(WriteObjectRequest)}.
    * Calls to {@link Flusher#flush(List)} will be sent but not synced.
@@ -121,6 +146,27 @@ final class WriteFlushStrategy {
     return b.build();
   }
 
+  private static BidiWriteObjectRequest possiblyPairDownBidiRequest(
+      BidiWriteObjectRequest message, boolean firstMessageOfStream) {
+    if (firstMessageOfStream && message.getWriteOffset() == 0) {
+      return message;
+    }
+
+    BidiWriteObjectRequest.Builder b = message.toBuilder();
+    if (!firstMessageOfStream) {
+      b.clearUploadId();
+    }
+
+    if (message.getWriteOffset() > 0) {
+      b.clearWriteObjectSpec();
+    }
+
+    if (message.getWriteOffset() > 0 && !message.getFinishWrite()) {
+      b.clearObjectChecksums();
+    }
+    return b.build();
+  }
+
   @FunctionalInterface
   interface FlusherFactory {
     /**
@@ -140,6 +186,25 @@ final class WriteFlushStrategy {
     void close(@Nullable WriteObjectRequest req);
   }
 
+  @FunctionalInterface
+  interface BidiFlusherFactory {
+    /**
+     * @param committedTotalBytesCallback Callback to signal the total number of bytes committed by
+     *     this flusher.
+     * @param onSuccessCallback Callback to signal success, and provide the final response.
+     */
+    BidiFlusher newFlusher(
+        String bucketName,
+        LongConsumer committedTotalBytesCallback,
+        Consumer<BidiWriteObjectResponse> onSuccessCallback);
+  }
+
+  interface BidiFlusher {
+    void flush(@NonNull List<BidiWriteObjectRequest> segments);
+
+    void close(@Nullable BidiWriteObjectRequest req);
+  }
+
   private static final class FsyncEveryFlusher implements Flusher {
 
     private final ClientStreamingCallable<WriteObjectRequest, WriteObjectResponse> write;
@@ -199,6 +264,82 @@ final class WriteFlushStrategy {
     }
   }
 
+  public static final class DefaultBidiFlusher implements BidiFlusher {
+
+    private final BidiStreamingCallable<BidiWriteObjectRequest, BidiWriteObjectResponse> write;
+    private final RetryingDependencies deps;
+    private final ResultRetryAlgorithm<?> alg;
+    private final String bucketName;
+    private final LongConsumer sizeCallback;
+    private final Consumer<BidiWriteObjectResponse> completeCallback;
+    private final Supplier<GrpcCallContext> baseContextSupplier;
+    private volatile ApiStreamObserver<BidiWriteObjectRequest> stream;
+
+    private final BidiObserver responseObserver;
+
+    private DefaultBidiFlusher(
+        BidiStreamingCallable<BidiWriteObjectRequest, BidiWriteObjectResponse> write,
+        RetryingDependencies deps,
+        ResultRetryAlgorithm<?> alg,
+        String bucketName,
+        LongConsumer sizeCallback,
+        Consumer<BidiWriteObjectResponse> completeCallback,
+        Supplier<GrpcCallContext> baseContextSupplier) {
+      this.write = write;
+      this.deps = deps;
+      this.alg = alg;
+      this.bucketName = bucketName;
+      this.sizeCallback = sizeCallback;
+      this.completeCallback = completeCallback;
+      this.baseContextSupplier = baseContextSupplier;
+      this.responseObserver = new BidiObserver(sizeCallback, completeCallback);
+    }
+
+    public void flush(@NonNull List<BidiWriteObjectRequest> segments) {
+      ensureOpen();
+      Retrying.run(
+          deps,
+          alg,
+          () -> {
+            boolean first = true;
+            for (BidiWriteObjectRequest message : segments) {
+              message = possiblyPairDownBidiRequest(message, first);
+
+              stream.onNext(message);
+              first = false;
+            }
+            BidiWriteObjectRequest message =
+                BidiWriteObjectRequest.newBuilder().setFlush(true).setStateLookup(true).build();
+            stream.onNext(message);
+            responseObserver.await();
+            return null;
+          },
+          Decoder.identity());
+    }
+
+    public void close(@Nullable BidiWriteObjectRequest req) {
+      ensureOpen();
+      if (req != null) {
+        flush(ImmutableList.of(req));
+      }
+    }
+
+    private void ensureOpen() {
+      if (stream == null) {
+        synchronized (this) {
+          if (stream == null) {
+            GrpcCallContext internalContext =
+                contextWithBucketName(bucketName, baseContextSupplier.get());
+            stream =
+                this.write
+                    .withDefaultCallContext(internalContext)
+                    .bidiStreamingCall(responseObserver);
+          }
+        }
+      }
+    }
+  }
+
   private static final class FsyncOnClose implements Flusher {
 
     private final ClientStreamingCallable<WriteObjectRequest, WriteObjectResponse> write;
@@ -315,4 +456,66 @@ final class WriteFlushStrategy {
       }
     }
   }
+
+  static class BidiObserver implements ApiStreamObserver<BidiWriteObjectResponse> {
+
+    private final LongConsumer sizeCallback;
+    private final Consumer<BidiWriteObjectResponse> completeCallback;
+
+    private final SettableApiFuture<Void> invocationHandle;
+    private volatile BidiWriteObjectResponse last;
+
+    BidiObserver(LongConsumer sizeCallback, Consumer<BidiWriteObjectResponse> completeCallback) {
+      this.sizeCallback = sizeCallback;
+      this.completeCallback = completeCallback;
+      this.invocationHandle = SettableApiFuture.create();
+    }
+
+    @Override
+    public void onNext(BidiWriteObjectResponse value) {
+      // incremental update
+      if (value.hasPersistedSize()) {
+        sizeCallback.accept(value.getPersistedSize());
+        invocationHandle.set(null);
+      } else if (value.hasResource()) {
+        sizeCallback.accept(value.getResource().getSize());
+      }
+      last = value;
+    }
+
+    /**
+     * observed exceptions so far
+     *
+     * <ol>
+     *   <li>{@link com.google.api.gax.rpc.OutOfRangeException}
+     *   <li>{@link com.google.api.gax.rpc.AlreadyExistsException}
+     *   <li>{@link io.grpc.StatusRuntimeException}
+     * </ol>
+     */
+    @Override
+    public void onError(Throwable t) {
+      invocationHandle.setException(t);
+    }
+
+    @Override
+    public void onCompleted() {
+
+      if (last != null && last.hasResource()) {
+        completeCallback.accept(last);
+      }
+      invocationHandle.set(null);
+    }
+
+    void await() {
+      try {
+        invocationHandle.get();
+      } catch (InterruptedException | ExecutionException e) {
+        if (e.getCause() instanceof RuntimeException) {
+          throw (RuntimeException) e.getCause();
+        } else {
+          throw new RuntimeException(e);
+        }
+      }
+    }
+  }
 }
diff --git a/pom.xml b/pom.xml
index 1e724000..23422704 100644
--- a/pom.xml
+++ b/pom.xml
@@ -185,7 +185,7 @@
       <dependency>
         <groupId>com.google.truth</groupId>
         <artifactId>truth</artifactId>
-        <version>1.4.0</version>
+        <version>1.4.1</version>
         <scope>test</scope>
         <exclusions>
           <exclusion>
diff --git a/proto-google-cloud-storage-control-v2/src/main/java/com/google/storage/control/v2/BucketName.java b/proto-google-cloud-storage-control-v2/src/main/java/com/google/storage/control/v2/BucketName.java
index 410eec92..7a301a30 100644
--- a/proto-google-cloud-storage-control-v2/src/main/java/com/google/storage/control/v2/BucketName.java
+++ b/proto-google-cloud-storage-control-v2/src/main/java/com/google/storage/control/v2/BucketName.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/proto-google-cloud-storage-control-v2/src/main/java/com/google/storage/control/v2/FolderName.java b/proto-google-cloud-storage-control-v2/src/main/java/com/google/storage/control/v2/FolderName.java
index a52b0a63..1fc3d295 100644
--- a/proto-google-cloud-storage-control-v2/src/main/java/com/google/storage/control/v2/FolderName.java
+++ b/proto-google-cloud-storage-control-v2/src/main/java/com/google/storage/control/v2/FolderName.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/proto-google-cloud-storage-control-v2/src/main/java/com/google/storage/control/v2/StorageLayoutName.java b/proto-google-cloud-storage-control-v2/src/main/java/com/google/storage/control/v2/StorageLayoutName.java
index 3e81f069..8aa4914d 100644
--- a/proto-google-cloud-storage-control-v2/src/main/java/com/google/storage/control/v2/StorageLayoutName.java
+++ b/proto-google-cloud-storage-control-v2/src/main/java/com/google/storage/control/v2/StorageLayoutName.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/proto-google-cloud-storage-v2/clirr-ignored-differences.xml b/proto-google-cloud-storage-v2/clirr-ignored-differences.xml
index 204313bf..32b659c4 100644
--- a/proto-google-cloud-storage-v2/clirr-ignored-differences.xml
+++ b/proto-google-cloud-storage-v2/clirr-ignored-differences.xml
@@ -55,4 +55,15 @@
     <className>com/google/storage/v2/*</className>
     <method>* *SoftDeleted*(*)</method>
   </difference>
+
+  <difference>
+    <differenceType>7012</differenceType>
+    <className>com/google/storage/v2/*</className>
+    <method>* *SoftDeleteTime*(*)</method>
+  </difference>
+  <difference>
+    <differenceType>7012</differenceType>
+    <className>com/google/storage/v2/*</className>
+    <method>* *HardDeleteTime*(*)</method>
+  </difference>
 </differences>
diff --git a/proto-google-cloud-storage-v2/src/main/java/com/google/storage/v2/BucketName.java b/proto-google-cloud-storage-v2/src/main/java/com/google/storage/v2/BucketName.java
index d2643c96..fec84c52 100644
--- a/proto-google-cloud-storage-v2/src/main/java/com/google/storage/v2/BucketName.java
+++ b/proto-google-cloud-storage-v2/src/main/java/com/google/storage/v2/BucketName.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/proto-google-cloud-storage-v2/src/main/java/com/google/storage/v2/CryptoKeyName.java b/proto-google-cloud-storage-v2/src/main/java/com/google/storage/v2/CryptoKeyName.java
index a9d2ffd7..cc44f3f4 100644
--- a/proto-google-cloud-storage-v2/src/main/java/com/google/storage/v2/CryptoKeyName.java
+++ b/proto-google-cloud-storage-v2/src/main/java/com/google/storage/v2/CryptoKeyName.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/proto-google-cloud-storage-v2/src/main/java/com/google/storage/v2/NotificationConfigName.java b/proto-google-cloud-storage-v2/src/main/java/com/google/storage/v2/NotificationConfigName.java
index 3a0c0a36..9d1091b7 100644
--- a/proto-google-cloud-storage-v2/src/main/java/com/google/storage/v2/NotificationConfigName.java
+++ b/proto-google-cloud-storage-v2/src/main/java/com/google/storage/v2/NotificationConfigName.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/proto-google-cloud-storage-v2/src/main/java/com/google/storage/v2/Object.java b/proto-google-cloud-storage-v2/src/main/java/com/google/storage/v2/Object.java
index 7748d810..13e834eb 100644
--- a/proto-google-cloud-storage-v2/src/main/java/com/google/storage/v2/Object.java
+++ b/proto-google-cloud-storage-v2/src/main/java/com/google/storage/v2/Object.java
@@ -1471,6 +1471,136 @@ public final class Object extends com.google.protobuf.GeneratedMessageV3
     return customTime_ == null ? com.google.protobuf.Timestamp.getDefaultInstance() : customTime_;
   }
 
+  public static final int SOFT_DELETE_TIME_FIELD_NUMBER = 28;
+  private com.google.protobuf.Timestamp softDeleteTime_;
+  /**
+   *
+   *
+   * <pre>
+   * Output only. This is the time when the object became soft-deleted.
+   *
+   * Soft-deleted objects are only accessible if a soft_delete_policy is
+   * enabled. Also see hard_delete_time.
+   * </pre>
+   *
+   * <code>
+   * optional .google.protobuf.Timestamp soft_delete_time = 28 [(.google.api.field_behavior) = OUTPUT_ONLY];
+   * </code>
+   *
+   * @return Whether the softDeleteTime field is set.
+   */
+  @java.lang.Override
+  public boolean hasSoftDeleteTime() {
+    return ((bitField0_ & 0x00000400) != 0);
+  }
+  /**
+   *
+   *
+   * <pre>
+   * Output only. This is the time when the object became soft-deleted.
+   *
+   * Soft-deleted objects are only accessible if a soft_delete_policy is
+   * enabled. Also see hard_delete_time.
+   * </pre>
+   *
+   * <code>
+   * optional .google.protobuf.Timestamp soft_delete_time = 28 [(.google.api.field_behavior) = OUTPUT_ONLY];
+   * </code>
+   *
+   * @return The softDeleteTime.
+   */
+  @java.lang.Override
+  public com.google.protobuf.Timestamp getSoftDeleteTime() {
+    return softDeleteTime_ == null
+        ? com.google.protobuf.Timestamp.getDefaultInstance()
+        : softDeleteTime_;
+  }
+  /**
+   *
+   *
+   * <pre>
+   * Output only. This is the time when the object became soft-deleted.
+   *
+   * Soft-deleted objects are only accessible if a soft_delete_policy is
+   * enabled. Also see hard_delete_time.
+   * </pre>
+   *
+   * <code>
+   * optional .google.protobuf.Timestamp soft_delete_time = 28 [(.google.api.field_behavior) = OUTPUT_ONLY];
+   * </code>
+   */
+  @java.lang.Override
+  public com.google.protobuf.TimestampOrBuilder getSoftDeleteTimeOrBuilder() {
+    return softDeleteTime_ == null
+        ? com.google.protobuf.Timestamp.getDefaultInstance()
+        : softDeleteTime_;
+  }
+
+  public static final int HARD_DELETE_TIME_FIELD_NUMBER = 29;
+  private com.google.protobuf.Timestamp hardDeleteTime_;
+  /**
+   *
+   *
+   * <pre>
+   * Output only. The time when the object will be permanently deleted.
+   *
+   * Only set when an object becomes soft-deleted with a soft_delete_policy.
+   * Otherwise, the object will not be accessible.
+   * </pre>
+   *
+   * <code>
+   * optional .google.protobuf.Timestamp hard_delete_time = 29 [(.google.api.field_behavior) = OUTPUT_ONLY];
+   * </code>
+   *
+   * @return Whether the hardDeleteTime field is set.
+   */
+  @java.lang.Override
+  public boolean hasHardDeleteTime() {
+    return ((bitField0_ & 0x00000800) != 0);
+  }
+  /**
+   *
+   *
+   * <pre>
+   * Output only. The time when the object will be permanently deleted.
+   *
+   * Only set when an object becomes soft-deleted with a soft_delete_policy.
+   * Otherwise, the object will not be accessible.
+   * </pre>
+   *
+   * <code>
+   * optional .google.protobuf.Timestamp hard_delete_time = 29 [(.google.api.field_behavior) = OUTPUT_ONLY];
+   * </code>
+   *
+   * @return The hardDeleteTime.
+   */
+  @java.lang.Override
+  public com.google.protobuf.Timestamp getHardDeleteTime() {
+    return hardDeleteTime_ == null
+        ? com.google.protobuf.Timestamp.getDefaultInstance()
+        : hardDeleteTime_;
+  }
+  /**
+   *
+   *
+   * <pre>
+   * Output only. The time when the object will be permanently deleted.
+   *
+   * Only set when an object becomes soft-deleted with a soft_delete_policy.
+   * Otherwise, the object will not be accessible.
+   * </pre>
+   *
+   * <code>
+   * optional .google.protobuf.Timestamp hard_delete_time = 29 [(.google.api.field_behavior) = OUTPUT_ONLY];
+   * </code>
+   */
+  @java.lang.Override
+  public com.google.protobuf.TimestampOrBuilder getHardDeleteTimeOrBuilder() {
+    return hardDeleteTime_ == null
+        ? com.google.protobuf.Timestamp.getDefaultInstance()
+        : hardDeleteTime_;
+  }
+
   private byte memoizedIsInitialized = -1;
 
   @java.lang.Override
@@ -1565,6 +1695,12 @@ public final class Object extends com.google.protobuf.GeneratedMessageV3
     if (!com.google.protobuf.GeneratedMessageV3.isStringEmpty(etag_)) {
       com.google.protobuf.GeneratedMessageV3.writeString(output, 27, etag_);
     }
+    if (((bitField0_ & 0x00000400) != 0)) {
+      output.writeMessage(28, getSoftDeleteTime());
+    }
+    if (((bitField0_ & 0x00000800) != 0)) {
+      output.writeMessage(29, getHardDeleteTime());
+    }
     getUnknownFields().writeTo(output);
   }
 
@@ -1664,6 +1800,12 @@ public final class Object extends com.google.protobuf.GeneratedMessageV3
     if (!com.google.protobuf.GeneratedMessageV3.isStringEmpty(etag_)) {
       size += com.google.protobuf.GeneratedMessageV3.computeStringSize(27, etag_);
     }
+    if (((bitField0_ & 0x00000400) != 0)) {
+      size += com.google.protobuf.CodedOutputStream.computeMessageSize(28, getSoftDeleteTime());
+    }
+    if (((bitField0_ & 0x00000800) != 0)) {
+      size += com.google.protobuf.CodedOutputStream.computeMessageSize(29, getHardDeleteTime());
+    }
     size += getUnknownFields().getSerializedSize();
     memoizedSize = size;
     return size;
@@ -1736,6 +1878,14 @@ public final class Object extends com.google.protobuf.GeneratedMessageV3
     if (hasCustomTime()) {
       if (!getCustomTime().equals(other.getCustomTime())) return false;
     }
+    if (hasSoftDeleteTime() != other.hasSoftDeleteTime()) return false;
+    if (hasSoftDeleteTime()) {
+      if (!getSoftDeleteTime().equals(other.getSoftDeleteTime())) return false;
+    }
+    if (hasHardDeleteTime() != other.hasHardDeleteTime()) return false;
+    if (hasHardDeleteTime()) {
+      if (!getHardDeleteTime().equals(other.getHardDeleteTime())) return false;
+    }
     if (!getUnknownFields().equals(other.getUnknownFields())) return false;
     return true;
   }
@@ -1825,6 +1975,14 @@ public final class Object extends com.google.protobuf.GeneratedMessageV3
       hash = (37 * hash) + CUSTOM_TIME_FIELD_NUMBER;
       hash = (53 * hash) + getCustomTime().hashCode();
     }
+    if (hasSoftDeleteTime()) {
+      hash = (37 * hash) + SOFT_DELETE_TIME_FIELD_NUMBER;
+      hash = (53 * hash) + getSoftDeleteTime().hashCode();
+    }
+    if (hasHardDeleteTime()) {
+      hash = (37 * hash) + HARD_DELETE_TIME_FIELD_NUMBER;
+      hash = (53 * hash) + getHardDeleteTime().hashCode();
+    }
     hash = (29 * hash) + getUnknownFields().hashCode();
     memoizedHashCode = hash;
     return hash;
@@ -1994,6 +2152,8 @@ public final class Object extends com.google.protobuf.GeneratedMessageV3
         getOwnerFieldBuilder();
         getCustomerEncryptionFieldBuilder();
         getCustomTimeFieldBuilder();
+        getSoftDeleteTimeFieldBuilder();
+        getHardDeleteTimeFieldBuilder();
       }
     }
 
@@ -2070,6 +2230,16 @@ public final class Object extends com.google.protobuf.GeneratedMessageV3
         customTimeBuilder_.dispose();
         customTimeBuilder_ = null;
       }
+      softDeleteTime_ = null;
+      if (softDeleteTimeBuilder_ != null) {
+        softDeleteTimeBuilder_.dispose();
+        softDeleteTimeBuilder_ = null;
+      }
+      hardDeleteTime_ = null;
+      if (hardDeleteTimeBuilder_ != null) {
+        hardDeleteTimeBuilder_.dispose();
+        hardDeleteTimeBuilder_ = null;
+      }
       return this;
     }
 
@@ -2216,6 +2386,16 @@ public final class Object extends com.google.protobuf.GeneratedMessageV3
         result.customTime_ = customTimeBuilder_ == null ? customTime_ : customTimeBuilder_.build();
         to_bitField0_ |= 0x00000200;
       }
+      if (((from_bitField0_ & 0x08000000) != 0)) {
+        result.softDeleteTime_ =
+            softDeleteTimeBuilder_ == null ? softDeleteTime_ : softDeleteTimeBuilder_.build();
+        to_bitField0_ |= 0x00000400;
+      }
+      if (((from_bitField0_ & 0x10000000) != 0)) {
+        result.hardDeleteTime_ =
+            hardDeleteTimeBuilder_ == null ? hardDeleteTime_ : hardDeleteTimeBuilder_.build();
+        to_bitField0_ |= 0x00000800;
+      }
       result.bitField0_ |= to_bitField0_;
     }
 
@@ -2388,6 +2568,12 @@ public final class Object extends com.google.protobuf.GeneratedMessageV3
       if (other.hasCustomTime()) {
         mergeCustomTime(other.getCustomTime());
       }
+      if (other.hasSoftDeleteTime()) {
+        mergeSoftDeleteTime(other.getSoftDeleteTime());
+      }
+      if (other.hasHardDeleteTime()) {
+        mergeHardDeleteTime(other.getHardDeleteTime());
+      }
       this.mergeUnknownFields(other.getUnknownFields());
       onChanged();
       return this;
@@ -2592,6 +2778,18 @@ public final class Object extends com.google.protobuf.GeneratedMessageV3
                 bitField0_ |= 0x00000004;
                 break;
               } // case 218
+            case 226:
+              {
+                input.readMessage(getSoftDeleteTimeFieldBuilder().getBuilder(), extensionRegistry);
+                bitField0_ |= 0x08000000;
+                break;
+              } // case 226
+            case 234:
+              {
+                input.readMessage(getHardDeleteTimeFieldBuilder().getBuilder(), extensionRegistry);
+                bitField0_ |= 0x10000000;
+                break;
+              } // case 234
             default:
               {
                 if (!super.parseUnknownField(input, extensionRegistry, tag)) {
@@ -6634,6 +6832,466 @@ public final class Object extends com.google.protobuf.GeneratedMessageV3
       return customTimeBuilder_;
     }
 
+    private com.google.protobuf.Timestamp softDeleteTime_;
+    private com.google.protobuf.SingleFieldBuilderV3<
+            com.google.protobuf.Timestamp,
+            com.google.protobuf.Timestamp.Builder,
+            com.google.protobuf.TimestampOrBuilder>
+        softDeleteTimeBuilder_;
+    /**
+     *
+     *
+     * <pre>
+     * Output only. This is the time when the object became soft-deleted.
+     *
+     * Soft-deleted objects are only accessible if a soft_delete_policy is
+     * enabled. Also see hard_delete_time.
+     * </pre>
+     *
+     * <code>
+     * optional .google.protobuf.Timestamp soft_delete_time = 28 [(.google.api.field_behavior) = OUTPUT_ONLY];
+     * </code>
+     *
+     * @return Whether the softDeleteTime field is set.
+     */
+    public boolean hasSoftDeleteTime() {
+      return ((bitField0_ & 0x08000000) != 0);
+    }
+    /**
+     *
+     *
+     * <pre>
+     * Output only. This is the time when the object became soft-deleted.
+     *
+     * Soft-deleted objects are only accessible if a soft_delete_policy is
+     * enabled. Also see hard_delete_time.
+     * </pre>
+     *
+     * <code>
+     * optional .google.protobuf.Timestamp soft_delete_time = 28 [(.google.api.field_behavior) = OUTPUT_ONLY];
+     * </code>
+     *
+     * @return The softDeleteTime.
+     */
+    public com.google.protobuf.Timestamp getSoftDeleteTime() {
+      if (softDeleteTimeBuilder_ == null) {
+        return softDeleteTime_ == null
+            ? com.google.protobuf.Timestamp.getDefaultInstance()
+            : softDeleteTime_;
+      } else {
+        return softDeleteTimeBuilder_.getMessage();
+      }
+    }
+    /**
+     *
+     *
+     * <pre>
+     * Output only. This is the time when the object became soft-deleted.
+     *
+     * Soft-deleted objects are only accessible if a soft_delete_policy is
+     * enabled. Also see hard_delete_time.
+     * </pre>
+     *
+     * <code>
+     * optional .google.protobuf.Timestamp soft_delete_time = 28 [(.google.api.field_behavior) = OUTPUT_ONLY];
+     * </code>
+     */
+    public Builder setSoftDeleteTime(com.google.protobuf.Timestamp value) {
+      if (softDeleteTimeBuilder_ == null) {
+        if (value == null) {
+          throw new NullPointerException();
+        }
+        softDeleteTime_ = value;
+      } else {
+        softDeleteTimeBuilder_.setMessage(value);
+      }
+      bitField0_ |= 0x08000000;
+      onChanged();
+      return this;
+    }
+    /**
+     *
+     *
+     * <pre>
+     * Output only. This is the time when the object became soft-deleted.
+     *
+     * Soft-deleted objects are only accessible if a soft_delete_policy is
+     * enabled. Also see hard_delete_time.
+     * </pre>
+     *
+     * <code>
+     * optional .google.protobuf.Timestamp soft_delete_time = 28 [(.google.api.field_behavior) = OUTPUT_ONLY];
+     * </code>
+     */
+    public Builder setSoftDeleteTime(com.google.protobuf.Timestamp.Builder builderForValue) {
+      if (softDeleteTimeBuilder_ == null) {
+        softDeleteTime_ = builderForValue.build();
+      } else {
+        softDeleteTimeBuilder_.setMessage(builderForValue.build());
+      }
+      bitField0_ |= 0x08000000;
+      onChanged();
+      return this;
+    }
+    /**
+     *
+     *
+     * <pre>
+     * Output only. This is the time when the object became soft-deleted.
+     *
+     * Soft-deleted objects are only accessible if a soft_delete_policy is
+     * enabled. Also see hard_delete_time.
+     * </pre>
+     *
+     * <code>
+     * optional .google.protobuf.Timestamp soft_delete_time = 28 [(.google.api.field_behavior) = OUTPUT_ONLY];
+     * </code>
+     */
+    public Builder mergeSoftDeleteTime(com.google.protobuf.Timestamp value) {
+      if (softDeleteTimeBuilder_ == null) {
+        if (((bitField0_ & 0x08000000) != 0)
+            && softDeleteTime_ != null
+            && softDeleteTime_ != com.google.protobuf.Timestamp.getDefaultInstance()) {
+          getSoftDeleteTimeBuilder().mergeFrom(value);
+        } else {
+          softDeleteTime_ = value;
+        }
+      } else {
+        softDeleteTimeBuilder_.mergeFrom(value);
+      }
+      if (softDeleteTime_ != null) {
+        bitField0_ |= 0x08000000;
+        onChanged();
+      }
+      return this;
+    }
+    /**
+     *
+     *
+     * <pre>
+     * Output only. This is the time when the object became soft-deleted.
+     *
+     * Soft-deleted objects are only accessible if a soft_delete_policy is
+     * enabled. Also see hard_delete_time.
+     * </pre>
+     *
+     * <code>
+     * optional .google.protobuf.Timestamp soft_delete_time = 28 [(.google.api.field_behavior) = OUTPUT_ONLY];
+     * </code>
+     */
+    public Builder clearSoftDeleteTime() {
+      bitField0_ = (bitField0_ & ~0x08000000);
+      softDeleteTime_ = null;
+      if (softDeleteTimeBuilder_ != null) {
+        softDeleteTimeBuilder_.dispose();
+        softDeleteTimeBuilder_ = null;
+      }
+      onChanged();
+      return this;
+    }
+    /**
+     *
+     *
+     * <pre>
+     * Output only. This is the time when the object became soft-deleted.
+     *
+     * Soft-deleted objects are only accessible if a soft_delete_policy is
+     * enabled. Also see hard_delete_time.
+     * </pre>
+     *
+     * <code>
+     * optional .google.protobuf.Timestamp soft_delete_time = 28 [(.google.api.field_behavior) = OUTPUT_ONLY];
+     * </code>
+     */
+    public com.google.protobuf.Timestamp.Builder getSoftDeleteTimeBuilder() {
+      bitField0_ |= 0x08000000;
+      onChanged();
+      return getSoftDeleteTimeFieldBuilder().getBuilder();
+    }
+    /**
+     *
+     *
+     * <pre>
+     * Output only. This is the time when the object became soft-deleted.
+     *
+     * Soft-deleted objects are only accessible if a soft_delete_policy is
+     * enabled. Also see hard_delete_time.
+     * </pre>
+     *
+     * <code>
+     * optional .google.protobuf.Timestamp soft_delete_time = 28 [(.google.api.field_behavior) = OUTPUT_ONLY];
+     * </code>
+     */
+    public com.google.protobuf.TimestampOrBuilder getSoftDeleteTimeOrBuilder() {
+      if (softDeleteTimeBuilder_ != null) {
+        return softDeleteTimeBuilder_.getMessageOrBuilder();
+      } else {
+        return softDeleteTime_ == null
+            ? com.google.protobuf.Timestamp.getDefaultInstance()
+            : softDeleteTime_;
+      }
+    }
+    /**
+     *
+     *
+     * <pre>
+     * Output only. This is the time when the object became soft-deleted.
+     *
+     * Soft-deleted objects are only accessible if a soft_delete_policy is
+     * enabled. Also see hard_delete_time.
+     * </pre>
+     *
+     * <code>
+     * optional .google.protobuf.Timestamp soft_delete_time = 28 [(.google.api.field_behavior) = OUTPUT_ONLY];
+     * </code>
+     */
+    private com.google.protobuf.SingleFieldBuilderV3<
+            com.google.protobuf.Timestamp,
+            com.google.protobuf.Timestamp.Builder,
+            com.google.protobuf.TimestampOrBuilder>
+        getSoftDeleteTimeFieldBuilder() {
+      if (softDeleteTimeBuilder_ == null) {
+        softDeleteTimeBuilder_ =
+            new com.google.protobuf.SingleFieldBuilderV3<
+                com.google.protobuf.Timestamp,
+                com.google.protobuf.Timestamp.Builder,
+                com.google.protobuf.TimestampOrBuilder>(
+                getSoftDeleteTime(), getParentForChildren(), isClean());
+        softDeleteTime_ = null;
+      }
+      return softDeleteTimeBuilder_;
+    }
+
+    private com.google.protobuf.Timestamp hardDeleteTime_;
+    private com.google.protobuf.SingleFieldBuilderV3<
+            com.google.protobuf.Timestamp,
+            com.google.protobuf.Timestamp.Builder,
+            com.google.protobuf.TimestampOrBuilder>
+        hardDeleteTimeBuilder_;
+    /**
+     *
+     *
+     * <pre>
+     * Output only. The time when the object will be permanently deleted.
+     *
+     * Only set when an object becomes soft-deleted with a soft_delete_policy.
+     * Otherwise, the object will not be accessible.
+     * </pre>
+     *
+     * <code>
+     * optional .google.protobuf.Timestamp hard_delete_time = 29 [(.google.api.field_behavior) = OUTPUT_ONLY];
+     * </code>
+     *
+     * @return Whether the hardDeleteTime field is set.
+     */
+    public boolean hasHardDeleteTime() {
+      return ((bitField0_ & 0x10000000) != 0);
+    }
+    /**
+     *
+     *
+     * <pre>
+     * Output only. The time when the object will be permanently deleted.
+     *
+     * Only set when an object becomes soft-deleted with a soft_delete_policy.
+     * Otherwise, the object will not be accessible.
+     * </pre>
+     *
+     * <code>
+     * optional .google.protobuf.Timestamp hard_delete_time = 29 [(.google.api.field_behavior) = OUTPUT_ONLY];
+     * </code>
+     *
+     * @return The hardDeleteTime.
+     */
+    public com.google.protobuf.Timestamp getHardDeleteTime() {
+      if (hardDeleteTimeBuilder_ == null) {
+        return hardDeleteTime_ == null
+            ? com.google.protobuf.Timestamp.getDefaultInstance()
+            : hardDeleteTime_;
+      } else {
+        return hardDeleteTimeBuilder_.getMessage();
+      }
+    }
+    /**
+     *
+     *
+     * <pre>
+     * Output only. The time when the object will be permanently deleted.
+     *
+     * Only set when an object becomes soft-deleted with a soft_delete_policy.
+     * Otherwise, the object will not be accessible.
+     * </pre>
+     *
+     * <code>
+     * optional .google.protobuf.Timestamp hard_delete_time = 29 [(.google.api.field_behavior) = OUTPUT_ONLY];
+     * </code>
+     */
+    public Builder setHardDeleteTime(com.google.protobuf.Timestamp value) {
+      if (hardDeleteTimeBuilder_ == null) {
+        if (value == null) {
+          throw new NullPointerException();
+        }
+        hardDeleteTime_ = value;
+      } else {
+        hardDeleteTimeBuilder_.setMessage(value);
+      }
+      bitField0_ |= 0x10000000;
+      onChanged();
+      return this;
+    }
+    /**
+     *
+     *
+     * <pre>
+     * Output only. The time when the object will be permanently deleted.
+     *
+     * Only set when an object becomes soft-deleted with a soft_delete_policy.
+     * Otherwise, the object will not be accessible.
+     * </pre>
+     *
+     * <code>
+     * optional .google.protobuf.Timestamp hard_delete_time = 29 [(.google.api.field_behavior) = OUTPUT_ONLY];
+     * </code>
+     */
+    public Builder setHardDeleteTime(com.google.protobuf.Timestamp.Builder builderForValue) {
+      if (hardDeleteTimeBuilder_ == null) {
+        hardDeleteTime_ = builderForValue.build();
+      } else {
+        hardDeleteTimeBuilder_.setMessage(builderForValue.build());
+      }
+      bitField0_ |= 0x10000000;
+      onChanged();
+      return this;
+    }
+    /**
+     *
+     *
+     * <pre>
+     * Output only. The time when the object will be permanently deleted.
+     *
+     * Only set when an object becomes soft-deleted with a soft_delete_policy.
+     * Otherwise, the object will not be accessible.
+     * </pre>
+     *
+     * <code>
+     * optional .google.protobuf.Timestamp hard_delete_time = 29 [(.google.api.field_behavior) = OUTPUT_ONLY];
+     * </code>
+     */
+    public Builder mergeHardDeleteTime(com.google.protobuf.Timestamp value) {
+      if (hardDeleteTimeBuilder_ == null) {
+        if (((bitField0_ & 0x10000000) != 0)
+            && hardDeleteTime_ != null
+            && hardDeleteTime_ != com.google.protobuf.Timestamp.getDefaultInstance()) {
+          getHardDeleteTimeBuilder().mergeFrom(value);
+        } else {
+          hardDeleteTime_ = value;
+        }
+      } else {
+        hardDeleteTimeBuilder_.mergeFrom(value);
+      }
+      if (hardDeleteTime_ != null) {
+        bitField0_ |= 0x10000000;
+        onChanged();
+      }
+      return this;
+    }
+    /**
+     *
+     *
+     * <pre>
+     * Output only. The time when the object will be permanently deleted.
+     *
+     * Only set when an object becomes soft-deleted with a soft_delete_policy.
+     * Otherwise, the object will not be accessible.
+     * </pre>
+     *
+     * <code>
+     * optional .google.protobuf.Timestamp hard_delete_time = 29 [(.google.api.field_behavior) = OUTPUT_ONLY];
+     * </code>
+     */
+    public Builder clearHardDeleteTime() {
+      bitField0_ = (bitField0_ & ~0x10000000);
+      hardDeleteTime_ = null;
+      if (hardDeleteTimeBuilder_ != null) {
+        hardDeleteTimeBuilder_.dispose();
+        hardDeleteTimeBuilder_ = null;
+      }
+      onChanged();
+      return this;
+    }
+    /**
+     *
+     *
+     * <pre>
+     * Output only. The time when the object will be permanently deleted.
+     *
+     * Only set when an object becomes soft-deleted with a soft_delete_policy.
+     * Otherwise, the object will not be accessible.
+     * </pre>
+     *
+     * <code>
+     * optional .google.protobuf.Timestamp hard_delete_time = 29 [(.google.api.field_behavior) = OUTPUT_ONLY];
+     * </code>
+     */
+    public com.google.protobuf.Timestamp.Builder getHardDeleteTimeBuilder() {
+      bitField0_ |= 0x10000000;
+      onChanged();
+      return getHardDeleteTimeFieldBuilder().getBuilder();
+    }
+    /**
+     *
+     *
+     * <pre>
+     * Output only. The time when the object will be permanently deleted.
+     *
+     * Only set when an object becomes soft-deleted with a soft_delete_policy.
+     * Otherwise, the object will not be accessible.
+     * </pre>
+     *
+     * <code>
+     * optional .google.protobuf.Timestamp hard_delete_time = 29 [(.google.api.field_behavior) = OUTPUT_ONLY];
+     * </code>
+     */
+    public com.google.protobuf.TimestampOrBuilder getHardDeleteTimeOrBuilder() {
+      if (hardDeleteTimeBuilder_ != null) {
+        return hardDeleteTimeBuilder_.getMessageOrBuilder();
+      } else {
+        return hardDeleteTime_ == null
+            ? com.google.protobuf.Timestamp.getDefaultInstance()
+            : hardDeleteTime_;
+      }
+    }
+    /**
+     *
+     *
+     * <pre>
+     * Output only. The time when the object will be permanently deleted.
+     *
+     * Only set when an object becomes soft-deleted with a soft_delete_policy.
+     * Otherwise, the object will not be accessible.
+     * </pre>
+     *
+     * <code>
+     * optional .google.protobuf.Timestamp hard_delete_time = 29 [(.google.api.field_behavior) = OUTPUT_ONLY];
+     * </code>
+     */
+    private com.google.protobuf.SingleFieldBuilderV3<
+            com.google.protobuf.Timestamp,
+            com.google.protobuf.Timestamp.Builder,
+            com.google.protobuf.TimestampOrBuilder>
+        getHardDeleteTimeFieldBuilder() {
+      if (hardDeleteTimeBuilder_ == null) {
+        hardDeleteTimeBuilder_ =
+            new com.google.protobuf.SingleFieldBuilderV3<
+                com.google.protobuf.Timestamp,
+                com.google.protobuf.Timestamp.Builder,
+                com.google.protobuf.TimestampOrBuilder>(
+                getHardDeleteTime(), getParentForChildren(), isClean());
+        hardDeleteTime_ = null;
+      }
+      return hardDeleteTimeBuilder_;
+    }
+
     @java.lang.Override
     public final Builder setUnknownFields(final com.google.protobuf.UnknownFieldSet unknownFields) {
       return super.setUnknownFields(unknownFields);
diff --git a/proto-google-cloud-storage-v2/src/main/java/com/google/storage/v2/ObjectOrBuilder.java b/proto-google-cloud-storage-v2/src/main/java/com/google/storage/v2/ObjectOrBuilder.java
index ce96b444..db5ea216 100644
--- a/proto-google-cloud-storage-v2/src/main/java/com/google/storage/v2/ObjectOrBuilder.java
+++ b/proto-google-cloud-storage-v2/src/main/java/com/google/storage/v2/ObjectOrBuilder.java
@@ -939,4 +939,104 @@ public interface ObjectOrBuilder
    * <code>.google.protobuf.Timestamp custom_time = 26;</code>
    */
   com.google.protobuf.TimestampOrBuilder getCustomTimeOrBuilder();
+
+  /**
+   *
+   *
+   * <pre>
+   * Output only. This is the time when the object became soft-deleted.
+   *
+   * Soft-deleted objects are only accessible if a soft_delete_policy is
+   * enabled. Also see hard_delete_time.
+   * </pre>
+   *
+   * <code>
+   * optional .google.protobuf.Timestamp soft_delete_time = 28 [(.google.api.field_behavior) = OUTPUT_ONLY];
+   * </code>
+   *
+   * @return Whether the softDeleteTime field is set.
+   */
+  boolean hasSoftDeleteTime();
+  /**
+   *
+   *
+   * <pre>
+   * Output only. This is the time when the object became soft-deleted.
+   *
+   * Soft-deleted objects are only accessible if a soft_delete_policy is
+   * enabled. Also see hard_delete_time.
+   * </pre>
+   *
+   * <code>
+   * optional .google.protobuf.Timestamp soft_delete_time = 28 [(.google.api.field_behavior) = OUTPUT_ONLY];
+   * </code>
+   *
+   * @return The softDeleteTime.
+   */
+  com.google.protobuf.Timestamp getSoftDeleteTime();
+  /**
+   *
+   *
+   * <pre>
+   * Output only. This is the time when the object became soft-deleted.
+   *
+   * Soft-deleted objects are only accessible if a soft_delete_policy is
+   * enabled. Also see hard_delete_time.
+   * </pre>
+   *
+   * <code>
+   * optional .google.protobuf.Timestamp soft_delete_time = 28 [(.google.api.field_behavior) = OUTPUT_ONLY];
+   * </code>
+   */
+  com.google.protobuf.TimestampOrBuilder getSoftDeleteTimeOrBuilder();
+
+  /**
+   *
+   *
+   * <pre>
+   * Output only. The time when the object will be permanently deleted.
+   *
+   * Only set when an object becomes soft-deleted with a soft_delete_policy.
+   * Otherwise, the object will not be accessible.
+   * </pre>
+   *
+   * <code>
+   * optional .google.protobuf.Timestamp hard_delete_time = 29 [(.google.api.field_behavior) = OUTPUT_ONLY];
+   * </code>
+   *
+   * @return Whether the hardDeleteTime field is set.
+   */
+  boolean hasHardDeleteTime();
+  /**
+   *
+   *
+   * <pre>
+   * Output only. The time when the object will be permanently deleted.
+   *
+   * Only set when an object becomes soft-deleted with a soft_delete_policy.
+   * Otherwise, the object will not be accessible.
+   * </pre>
+   *
+   * <code>
+   * optional .google.protobuf.Timestamp hard_delete_time = 29 [(.google.api.field_behavior) = OUTPUT_ONLY];
+   * </code>
+   *
+   * @return The hardDeleteTime.
+   */
+  com.google.protobuf.Timestamp getHardDeleteTime();
+  /**
+   *
+   *
+   * <pre>
+   * Output only. The time when the object will be permanently deleted.
+   *
+   * Only set when an object becomes soft-deleted with a soft_delete_policy.
+   * Otherwise, the object will not be accessible.
+   * </pre>
+   *
+   * <code>
+   * optional .google.protobuf.Timestamp hard_delete_time = 29 [(.google.api.field_behavior) = OUTPUT_ONLY];
+   * </code>
+   */
+  com.google.protobuf.TimestampOrBuilder getHardDeleteTimeOrBuilder();
 }
diff --git a/proto-google-cloud-storage-v2/src/main/java/com/google/storage/v2/ProjectName.java b/proto-google-cloud-storage-v2/src/main/java/com/google/storage/v2/ProjectName.java
index b8d7aa7b..1788c480 100644
--- a/proto-google-cloud-storage-v2/src/main/java/com/google/storage/v2/ProjectName.java
+++ b/proto-google-cloud-storage-v2/src/main/java/com/google/storage/v2/ProjectName.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/proto-google-cloud-storage-v2/src/main/java/com/google/storage/v2/StorageProto.java b/proto-google-cloud-storage-v2/src/main/java/com/google/storage/v2/StorageProto.java
index 6d6bbc9b..7c9d8f22 100644
--- a/proto-google-cloud-storage-v2/src/main/java/com/google/storage/v2/StorageProto.java
+++ b/proto-google-cloud-storage-v2/src/main/java/com/google/storage/v2/StorageProto.java
@@ -762,7 +762,7 @@ public final class StorageProto {
           + "/{project}/buckets/{bucket}/notification"
           + "Configs/{notification_config}\"L\n\022Custome"
           + "rEncryption\022\034\n\024encryption_algorithm\030\001 \001("
-          + "\t\022\030\n\020key_sha256_bytes\030\003 \001(\014\"\202\t\n\006Object\022\021"
+          + "\t\022\030\n\020key_sha256_bytes\030\003 \001(\014\"\254\n\n\006Object\022\021"
           + "\n\004name\030\001 \001(\tB\003\340A\005\0225\n\006bucket\030\002 \001(\tB%\340A\005\372A"
           + "\037\n\035storage.googleapis.com/Bucket\022\014\n\004etag"
           + "\030\033 \001(\t\022\027\n\ngeneration\030\003 \001(\003B\003\340A\005\022\033\n\016metag"
@@ -789,154 +789,158 @@ public final class StorageProto {
           + "\030\030 \001(\0132\030.google.storage.v2.OwnerB\003\340A\003\022B\n"
           + "\023customer_encryption\030\031 \001(\0132%.google.stor"
           + "age.v2.CustomerEncryption\022/\n\013custom_time"
-          + "\030\032 \001(\0132\032.google.protobuf.Timestamp\032/\n\rMe"
-          + "tadataEntry\022\013\n\003key\030\001 \001(\t\022\r\n\005value\030\002 \001(\t:"
-          + "\0028\001B\023\n\021_event_based_hold\"\316\001\n\023ObjectAcces"
-          + "sControl\022\014\n\004role\030\001 \001(\t\022\n\n\002id\030\002 \001(\t\022\016\n\006en"
-          + "tity\030\003 \001(\t\022\027\n\nentity_alt\030\t \001(\tB\003\340A\003\022\021\n\te"
-          + "ntity_id\030\004 \001(\t\022\014\n\004etag\030\010 \001(\t\022\r\n\005email\030\005 "
-          + "\001(\t\022\016\n\006domain\030\006 \001(\t\0224\n\014project_team\030\007 \001("
-          + "\0132\036.google.storage.v2.ProjectTeam\"l\n\023Lis"
-          + "tObjectsResponse\022*\n\007objects\030\001 \003(\0132\031.goog"
-          + "le.storage.v2.Object\022\020\n\010prefixes\030\002 \003(\t\022\027"
-          + "\n\017next_page_token\030\003 \001(\t\"3\n\013ProjectTeam\022\026"
-          + "\n\016project_number\030\001 \001(\t\022\014\n\004team\030\002 \001(\t\"\'\n\016"
-          + "ServiceAccount\022\025\n\remail_address\030\001 \001(\t\"*\n"
-          + "\005Owner\022\016\n\006entity\030\001 \001(\t\022\021\n\tentity_id\030\002 \001("
-          + "\t\"C\n\014ContentRange\022\r\n\005start\030\001 \001(\003\022\013\n\003end\030"
-          + "\002 \001(\003\022\027\n\017complete_length\030\003 \001(\0032\252\'\n\007Stora"
-          + "ge\022r\n\014DeleteBucket\022&.google.storage.v2.D"
-          + "eleteBucketRequest\032\026.google.protobuf.Emp"
-          + "ty\"\"\332A\004name\212\323\344\223\002\025\022\023\n\004name\022\013{bucket=**}\022o"
-          + "\n\tGetBucket\022#.google.storage.v2.GetBucke"
-          + "tRequest\032\031.google.storage.v2.Bucket\"\"\332A\004"
-          + "name\212\323\344\223\002\025\022\023\n\004name\022\013{bucket=**}\022\253\001\n\014Crea"
-          + "teBucket\022&.google.storage.v2.CreateBucke"
-          + "tRequest\032\031.google.storage.v2.Bucket\"X\332A\027"
-          + "parent,bucket,bucket_id\212\323\344\223\0028\022\026\n\006parent\022"
-          + "\014{project=**}\022\036\n\016bucket.project\022\014{projec"
-          + "t=**}\022\205\001\n\013ListBuckets\022%.google.storage.v"
-          + "2.ListBucketsRequest\032&.google.storage.v2"
-          + ".ListBucketsResponse\"\'\332A\006parent\212\323\344\223\002\030\022\026\n"
-          + "\006parent\022\014{project=**}\022\223\001\n\031LockBucketRete"
-          + "ntionPolicy\0223.google.storage.v2.LockBuck"
-          + "etRetentionPolicyRequest\032\031.google.storag"
-          + "e.v2.Bucket\"&\332A\006bucket\212\323\344\223\002\027\022\025\n\006bucket\022\013"
-          + "{bucket=**}\022u\n\014GetIamPolicy\022\".google.iam"
-          + ".v1.GetIamPolicyRequest\032\025.google.iam.v1."
-          + "Policy\"*\332A\010resource\212\323\344\223\002\031\022\027\n\010resource\022\013{"
-          + "bucket=**}\022|\n\014SetIamPolicy\022\".google.iam."
-          + "v1.SetIamPolicyRequest\032\025.google.iam.v1.P"
-          + "olicy\"1\332A\017resource,policy\212\323\344\223\002\031\022\027\n\010resou"
-          + "rce\022\013{bucket=**}\022\327\001\n\022TestIamPermissions\022"
-          + "(.google.iam.v1.TestIamPermissionsReques"
-          + "t\032).google.iam.v1.TestIamPermissionsResp"
-          + "onse\"l\332A\024resource,permissions\212\323\344\223\002O\022\027\n\010r"
-          + "esource\022\013{bucket=**}\0224\n\010resource\022({bucke"
-          + "t=projects/*/buckets/*}/objects/**\022\212\001\n\014U"
-          + "pdateBucket\022&.google.storage.v2.UpdateBu"
-          + "cketRequest\032\031.google.storage.v2.Bucket\"7"
-          + "\332A\022bucket,update_mask\212\323\344\223\002\034\022\032\n\013bucket.na"
-          + "me\022\013{bucket=**}\022\237\001\n\030DeleteNotificationCo"
-          + "nfig\0222.google.storage.v2.DeleteNotificat"
-          + "ionConfigRequest\032\026.google.protobuf.Empty"
-          + "\"7\332A\004name\212\323\344\223\002*\022(\n\004name\022 {bucket=project"
-          + "s/*/buckets/*}/**\022\250\001\n\025GetNotificationCon"
-          + "fig\022/.google.storage.v2.GetNotificationC"
-          + "onfigRequest\032%.google.storage.v2.Notific"
-          + "ationConfig\"7\332A\004name\212\323\344\223\002*\022(\n\004name\022 {buc"
-          + "ket=projects/*/buckets/*}/**\022\261\001\n\030CreateN"
-          + "otificationConfig\0222.google.storage.v2.Cr"
-          + "eateNotificationConfigRequest\032%.google.s"
-          + "torage.v2.NotificationConfig\":\332A\032parent,"
-          + "notification_config\212\323\344\223\002\027\022\025\n\006parent\022\013{bu"
-          + "cket=**}\022\250\001\n\027ListNotificationConfigs\0221.g"
-          + "oogle.storage.v2.ListNotificationConfigs"
-          + "Request\0322.google.storage.v2.ListNotifica"
-          + "tionConfigsResponse\"&\332A\006parent\212\323\344\223\002\027\022\025\n\006"
-          + "parent\022\013{bucket=**}\022~\n\rComposeObject\022\'.g"
-          + "oogle.storage.v2.ComposeObjectRequest\032\031."
-          + "google.storage.v2.Object\")\212\323\344\223\002#\022!\n\022dest"
-          + "ination.bucket\022\013{bucket=**}\022\230\001\n\014DeleteOb"
-          + "ject\022&.google.storage.v2.DeleteObjectReq"
-          + "uest\032\026.google.protobuf.Empty\"H\332A\rbucket,"
-          + "object\332A\030bucket,object,generation\212\323\344\223\002\027\022"
-          + "\025\n\006bucket\022\013{bucket=**}\022\215\001\n\rRestoreObject"
-          + "\022\'.google.storage.v2.RestoreObjectReques"
-          + "t\032\031.google.storage.v2.Object\"8\332A\030bucket,"
-          + "object,generation\212\323\344\223\002\027\022\025\n\006bucket\022\013{buck"
-          + "et=**}\022\272\001\n\024CancelResumableWrite\022..google"
-          + ".storage.v2.CancelResumableWriteRequest\032"
-          + "/.google.storage.v2.CancelResumableWrite"
-          + "Response\"A\332A\tupload_id\212\323\344\223\002/\022-\n\tupload_i"
-          + "d\022 {bucket=projects/*/buckets/*}/**\022\225\001\n\t"
-          + "GetObject\022#.google.storage.v2.GetObjectR"
-          + "equest\032\031.google.storage.v2.Object\"H\332A\rbu"
-          + "cket,object\332A\030bucket,object,generation\212\323"
-          + "\344\223\002\027\022\025\n\006bucket\022\013{bucket=**}\022\245\001\n\nReadObje"
-          + "ct\022$.google.storage.v2.ReadObjectRequest"
-          + "\032%.google.storage.v2.ReadObjectResponse\""
-          + "H\332A\rbucket,object\332A\030bucket,object,genera"
-          + "tion\212\323\344\223\002\027\022\025\n\006bucket\022\013{bucket=**}0\001\022\214\001\n\014"
-          + "UpdateObject\022&.google.storage.v2.UpdateO"
-          + "bjectRequest\032\031.google.storage.v2.Object\""
-          + "9\332A\022object,update_mask\212\323\344\223\002\036\022\034\n\robject.b"
-          + "ucket\022\013{bucket=**}\022`\n\013WriteObject\022%.goog"
-          + "le.storage.v2.WriteObjectRequest\032&.googl"
-          + "e.storage.v2.WriteObjectResponse\"\000(\001\022n\n\017"
-          + "BidiWriteObject\022).google.storage.v2.Bidi"
-          + "WriteObjectRequest\032*.google.storage.v2.B"
-          + "idiWriteObjectResponse\"\000(\0010\001\022\204\001\n\013ListObj"
-          + "ects\022%.google.storage.v2.ListObjectsRequ"
-          + "est\032&.google.storage.v2.ListObjectsRespo"
-          + "nse\"&\332A\006parent\212\323\344\223\002\027\022\025\n\006parent\022\013{bucket="
-          + "**}\022\230\001\n\rRewriteObject\022\'.google.storage.v"
-          + "2.RewriteObjectRequest\032\".google.storage."
-          + "v2.RewriteResponse\":\212\323\344\223\0024\022\017\n\rsource_buc"
-          + "ket\022!\n\022destination_bucket\022\013{bucket=**}\022\256"
-          + "\001\n\023StartResumableWrite\022-.google.storage."
-          + "v2.StartResumableWriteRequest\032..google.s"
-          + "torage.v2.StartResumableWriteResponse\"8\212"
-          + "\323\344\223\0022\0220\n!write_object_spec.resource.buck"
-          + "et\022\013{bucket=**}\022\256\001\n\020QueryWriteStatus\022*.g"
-          + "oogle.storage.v2.QueryWriteStatusRequest"
-          + "\032+.google.storage.v2.QueryWriteStatusRes"
-          + "ponse\"A\332A\tupload_id\212\323\344\223\002/\022-\n\tupload_id\022 "
-          + "{bucket=projects/*/buckets/*}/**\022\200\001\n\021Get"
-          + "ServiceAccount\022+.google.storage.v2.GetSe"
-          + "rviceAccountRequest\032!.google.storage.v2."
-          + "ServiceAccount\"\033\332A\007project\212\323\344\223\002\013\022\t\n\007proj"
-          + "ect\022\225\001\n\rCreateHmacKey\022\'.google.storage.v"
-          + "2.CreateHmacKeyRequest\032(.google.storage."
-          + "v2.CreateHmacKeyResponse\"1\332A\035project,ser"
-          + "vice_account_email\212\323\344\223\002\013\022\t\n\007project\022w\n\rD"
-          + "eleteHmacKey\022\'.google.storage.v2.DeleteH"
-          + "macKeyRequest\032\026.google.protobuf.Empty\"%\332"
-          + "A\021access_id,project\212\323\344\223\002\013\022\t\n\007project\022}\n\n"
-          + "GetHmacKey\022$.google.storage.v2.GetHmacKe"
-          + "yRequest\032\".google.storage.v2.HmacKeyMeta"
-          + "data\"%\332A\021access_id,project\212\323\344\223\002\013\022\t\n\007proj"
-          + "ect\022|\n\014ListHmacKeys\022&.google.storage.v2."
-          + "ListHmacKeysRequest\032\'.google.storage.v2."
-          + "ListHmacKeysResponse\"\033\332A\007project\212\323\344\223\002\013\022\t"
-          + "\n\007project\022\235\001\n\rUpdateHmacKey\022\'.google.sto"
-          + "rage.v2.UpdateHmacKeyRequest\032\".google.st"
-          + "orage.v2.HmacKeyMetadata\"?\332A\024hmac_key,up"
-          + "date_mask\212\323\344\223\002\"\022 \n\020hmac_key.project\022\014{pr"
-          + "oject=**}\032\247\002\312A\026storage.googleapis.com\322A\212"
-          + "\002https://www.googleapis.com/auth/cloud-p"
-          + "latform,https://www.googleapis.com/auth/"
-          + "cloud-platform.read-only,https://www.goo"
-          + "gleapis.com/auth/devstorage.full_control"
-          + ",https://www.googleapis.com/auth/devstor"
-          + "age.read_only,https://www.googleapis.com"
-          + "/auth/devstorage.read_writeB\342\001\n\025com.goog"
-          + "le.storage.v2B\014StorageProtoP\001Z>cloud.goo"
-          + "gle.com/go/storage/internal/apiv2/storag"
-          + "epb;storagepb\352Ax\n!cloudkms.googleapis.co"
-          + "m/CryptoKey\022Sprojects/{project}/location"
-          + "s/{location}/keyRings/{key_ring}/cryptoK"
-          + "eys/{crypto_key}b\006proto3"
+          + "\030\032 \001(\0132\032.google.protobuf.Timestamp\022>\n\020so"
+          + "ft_delete_time\030\034 \001(\0132\032.google.protobuf.T"
+          + "imestampB\003\340A\003H\001\210\001\001\022>\n\020hard_delete_time\030\035"
+          + " \001(\0132\032.google.protobuf.TimestampB\003\340A\003H\002\210"
+          + "\001\001\032/\n\rMetadataEntry\022\013\n\003key\030\001 \001(\t\022\r\n\005valu"
+          + "e\030\002 \001(\t:\0028\001B\023\n\021_event_based_holdB\023\n\021_sof"
+          + "t_delete_timeB\023\n\021_hard_delete_time\"\316\001\n\023O"
+          + "bjectAccessControl\022\014\n\004role\030\001 \001(\t\022\n\n\002id\030\002"
+          + " \001(\t\022\016\n\006entity\030\003 \001(\t\022\027\n\nentity_alt\030\t \001(\t"
+          + "B\003\340A\003\022\021\n\tentity_id\030\004 \001(\t\022\014\n\004etag\030\010 \001(\t\022\r"
+          + "\n\005email\030\005 \001(\t\022\016\n\006domain\030\006 \001(\t\0224\n\014project"
+          + "_team\030\007 \001(\0132\036.google.storage.v2.ProjectT"
+          + "eam\"l\n\023ListObjectsResponse\022*\n\007objects\030\001 "
+          + "\003(\0132\031.google.storage.v2.Object\022\020\n\010prefix"
+          + "es\030\002 \003(\t\022\027\n\017next_page_token\030\003 \001(\t\"3\n\013Pro"
+          + "jectTeam\022\026\n\016project_number\030\001 \001(\t\022\014\n\004team"
+          + "\030\002 \001(\t\"\'\n\016ServiceAccount\022\025\n\remail_addres"
+          + "s\030\001 \001(\t\"*\n\005Owner\022\016\n\006entity\030\001 \001(\t\022\021\n\tenti"
+          + "ty_id\030\002 \001(\t\"C\n\014ContentRange\022\r\n\005start\030\001 \001"
+          + "(\003\022\013\n\003end\030\002 \001(\003\022\027\n\017complete_length\030\003 \001(\003"
+          + "2\252\'\n\007Storage\022r\n\014DeleteBucket\022&.google.st"
+          + "orage.v2.DeleteBucketRequest\032\026.google.pr"
+          + "otobuf.Empty\"\"\332A\004name\212\323\344\223\002\025\022\023\n\004name\022\013{bu"
+          + "cket=**}\022o\n\tGetBucket\022#.google.storage.v"
+          + "2.GetBucketRequest\032\031.google.storage.v2.B"
+          + "ucket\"\"\332A\004name\212\323\344\223\002\025\022\023\n\004name\022\013{bucket=**"
+          + "}\022\253\001\n\014CreateBucket\022&.google.storage.v2.C"
+          + "reateBucketRequest\032\031.google.storage.v2.B"
+          + "ucket\"X\332A\027parent,bucket,bucket_id\212\323\344\223\0028\022"
+          + "\026\n\006parent\022\014{project=**}\022\036\n\016bucket.projec"
+          + "t\022\014{project=**}\022\205\001\n\013ListBuckets\022%.google"
+          + ".storage.v2.ListBucketsRequest\032&.google."
+          + "storage.v2.ListBucketsResponse\"\'\332A\006paren"
+          + "t\212\323\344\223\002\030\022\026\n\006parent\022\014{project=**}\022\223\001\n\031Lock"
+          + "BucketRetentionPolicy\0223.google.storage.v"
+          + "2.LockBucketRetentionPolicyRequest\032\031.goo"
+          + "gle.storage.v2.Bucket\"&\332A\006bucket\212\323\344\223\002\027\022\025"
+          + "\n\006bucket\022\013{bucket=**}\022u\n\014GetIamPolicy\022\"."
+          + "google.iam.v1.GetIamPolicyRequest\032\025.goog"
+          + "le.iam.v1.Policy\"*\332A\010resource\212\323\344\223\002\031\022\027\n\010r"
+          + "esource\022\013{bucket=**}\022|\n\014SetIamPolicy\022\".g"
+          + "oogle.iam.v1.SetIamPolicyRequest\032\025.googl"
+          + "e.iam.v1.Policy\"1\332A\017resource,policy\212\323\344\223\002"
+          + "\031\022\027\n\010resource\022\013{bucket=**}\022\327\001\n\022TestIamPe"
+          + "rmissions\022(.google.iam.v1.TestIamPermiss"
+          + "ionsRequest\032).google.iam.v1.TestIamPermi"
+          + "ssionsResponse\"l\332A\024resource,permissions\212"
+          + "\323\344\223\002O\022\027\n\010resource\022\013{bucket=**}\0224\n\010resour"
+          + "ce\022({bucket=projects/*/buckets/*}/object"
+          + "s/**\022\212\001\n\014UpdateBucket\022&.google.storage.v"
+          + "2.UpdateBucketRequest\032\031.google.storage.v"
+          + "2.Bucket\"7\332A\022bucket,update_mask\212\323\344\223\002\034\022\032\n"
+          + "\013bucket.name\022\013{bucket=**}\022\237\001\n\030DeleteNoti"
+          + "ficationConfig\0222.google.storage.v2.Delet"
+          + "eNotificationConfigRequest\032\026.google.prot"
+          + "obuf.Empty\"7\332A\004name\212\323\344\223\002*\022(\n\004name\022 {buck"
+          + "et=projects/*/buckets/*}/**\022\250\001\n\025GetNotif"
+          + "icationConfig\022/.google.storage.v2.GetNot"
+          + "ificationConfigRequest\032%.google.storage."
+          + "v2.NotificationConfig\"7\332A\004name\212\323\344\223\002*\022(\n\004"
+          + "name\022 {bucket=projects/*/buckets/*}/**\022\261"
+          + "\001\n\030CreateNotificationConfig\0222.google.sto"
+          + "rage.v2.CreateNotificationConfigRequest\032"
+          + "%.google.storage.v2.NotificationConfig\":"
+          + "\332A\032parent,notification_config\212\323\344\223\002\027\022\025\n\006p"
+          + "arent\022\013{bucket=**}\022\250\001\n\027ListNotificationC"
+          + "onfigs\0221.google.storage.v2.ListNotificat"
+          + "ionConfigsRequest\0322.google.storage.v2.Li"
+          + "stNotificationConfigsResponse\"&\332A\006parent"
+          + "\212\323\344\223\002\027\022\025\n\006parent\022\013{bucket=**}\022~\n\rCompose"
+          + "Object\022\'.google.storage.v2.ComposeObject"
+          + "Request\032\031.google.storage.v2.Object\")\212\323\344\223"
+          + "\002#\022!\n\022destination.bucket\022\013{bucket=**}\022\230\001"
+          + "\n\014DeleteObject\022&.google.storage.v2.Delet"
+          + "eObjectRequest\032\026.google.protobuf.Empty\"H"
+          + "\332A\rbucket,object\332A\030bucket,object,generat"
+          + "ion\212\323\344\223\002\027\022\025\n\006bucket\022\013{bucket=**}\022\215\001\n\rRes"
+          + "toreObject\022\'.google.storage.v2.RestoreOb"
+          + "jectRequest\032\031.google.storage.v2.Object\"8"
+          + "\332A\030bucket,object,generation\212\323\344\223\002\027\022\025\n\006buc"
+          + "ket\022\013{bucket=**}\022\272\001\n\024CancelResumableWrit"
+          + "e\022..google.storage.v2.CancelResumableWri"
+          + "teRequest\032/.google.storage.v2.CancelResu"
+          + "mableWriteResponse\"A\332A\tupload_id\212\323\344\223\002/\022-"
+          + "\n\tupload_id\022 {bucket=projects/*/buckets/"
+          + "*}/**\022\225\001\n\tGetObject\022#.google.storage.v2."
+          + "GetObjectRequest\032\031.google.storage.v2.Obj"
+          + "ect\"H\332A\rbucket,object\332A\030bucket,object,ge"
+          + "neration\212\323\344\223\002\027\022\025\n\006bucket\022\013{bucket=**}\022\245\001"
+          + "\n\nReadObject\022$.google.storage.v2.ReadObj"
+          + "ectRequest\032%.google.storage.v2.ReadObjec"
+          + "tResponse\"H\332A\rbucket,object\332A\030bucket,obj"
+          + "ect,generation\212\323\344\223\002\027\022\025\n\006bucket\022\013{bucket="
+          + "**}0\001\022\214\001\n\014UpdateObject\022&.google.storage."
+          + "v2.UpdateObjectRequest\032\031.google.storage."
+          + "v2.Object\"9\332A\022object,update_mask\212\323\344\223\002\036\022\034"
+          + "\n\robject.bucket\022\013{bucket=**}\022`\n\013WriteObj"
+          + "ect\022%.google.storage.v2.WriteObjectReque"
+          + "st\032&.google.storage.v2.WriteObjectRespon"
+          + "se\"\000(\001\022n\n\017BidiWriteObject\022).google.stora"
+          + "ge.v2.BidiWriteObjectRequest\032*.google.st"
+          + "orage.v2.BidiWriteObjectResponse\"\000(\0010\001\022\204"
+          + "\001\n\013ListObjects\022%.google.storage.v2.ListO"
+          + "bjectsRequest\032&.google.storage.v2.ListOb"
+          + "jectsResponse\"&\332A\006parent\212\323\344\223\002\027\022\025\n\006parent"
+          + "\022\013{bucket=**}\022\230\001\n\rRewriteObject\022\'.google"
+          + ".storage.v2.RewriteObjectRequest\032\".googl"
+          + "e.storage.v2.RewriteResponse\":\212\323\344\223\0024\022\017\n\r"
+          + "source_bucket\022!\n\022destination_bucket\022\013{bu"
+          + "cket=**}\022\256\001\n\023StartResumableWrite\022-.googl"
+          + "e.storage.v2.StartResumableWriteRequest\032"
+          + "..google.storage.v2.StartResumableWriteR"
+          + "esponse\"8\212\323\344\223\0022\0220\n!write_object_spec.res"
+          + "ource.bucket\022\013{bucket=**}\022\256\001\n\020QueryWrite"
+          + "Status\022*.google.storage.v2.QueryWriteSta"
+          + "tusRequest\032+.google.storage.v2.QueryWrit"
+          + "eStatusResponse\"A\332A\tupload_id\212\323\344\223\002/\022-\n\tu"
+          + "pload_id\022 {bucket=projects/*/buckets/*}/"
+          + "**\022\200\001\n\021GetServiceAccount\022+.google.storag"
+          + "e.v2.GetServiceAccountRequest\032!.google.s"
+          + "torage.v2.ServiceAccount\"\033\332A\007project\212\323\344\223"
+          + "\002\013\022\t\n\007project\022\225\001\n\rCreateHmacKey\022\'.google"
+          + ".storage.v2.CreateHmacKeyRequest\032(.googl"
+          + "e.storage.v2.CreateHmacKeyResponse\"1\332A\035p"
+          + "roject,service_account_email\212\323\344\223\002\013\022\t\n\007pr"
+          + "oject\022w\n\rDeleteHmacKey\022\'.google.storage."
+          + "v2.DeleteHmacKeyRequest\032\026.google.protobu"
+          + "f.Empty\"%\332A\021access_id,project\212\323\344\223\002\013\022\t\n\007p"
+          + "roject\022}\n\nGetHmacKey\022$.google.storage.v2"
+          + ".GetHmacKeyRequest\032\".google.storage.v2.H"
+          + "macKeyMetadata\"%\332A\021access_id,project\212\323\344\223"
+          + "\002\013\022\t\n\007project\022|\n\014ListHmacKeys\022&.google.s"
+          + "torage.v2.ListHmacKeysRequest\032\'.google.s"
+          + "torage.v2.ListHmacKeysResponse\"\033\332A\007proje"
+          + "ct\212\323\344\223\002\013\022\t\n\007project\022\235\001\n\rUpdateHmacKey\022\'."
+          + "google.storage.v2.UpdateHmacKeyRequest\032\""
+          + ".google.storage.v2.HmacKeyMetadata\"?\332A\024h"
+          + "mac_key,update_mask\212\323\344\223\002\"\022 \n\020hmac_key.pr"
+          + "oject\022\014{project=**}\032\247\002\312A\026storage.googlea"
+          + "pis.com\322A\212\002https://www.googleapis.com/au"
+          + "th/cloud-platform,https://www.googleapis"
+          + ".com/auth/cloud-platform.read-only,https"
+          + "://www.googleapis.com/auth/devstorage.fu"
+          + "ll_control,https://www.googleapis.com/au"
+          + "th/devstorage.read_only,https://www.goog"
+          + "leapis.com/auth/devstorage.read_writeB\342\001"
+          + "\n\025com.google.storage.v2B\014StorageProtoP\001Z"
+          + ">cloud.google.com/go/storage/internal/ap"
+          + "iv2/storagepb;storagepb\352Ax\n!cloudkms.goo"
+          + "gleapis.com/CryptoKey\022Sprojects/{project"
+          + "}/locations/{location}/keyRings/{key_rin"
+          + "g}/cryptoKeys/{crypto_key}b\006proto3"
     };
     descriptor =
         com.google.protobuf.Descriptors.FileDescriptor.internalBuildGeneratedFileFrom(
@@ -1712,6 +1716,8 @@ public final class StorageProto {
               "Owner",
               "CustomerEncryption",
               "CustomTime",
+              "SoftDeleteTime",
+              "HardDeleteTime",
             });
     internal_static_google_storage_v2_Object_MetadataEntry_descriptor =
         internal_static_google_storage_v2_Object_descriptor.getNestedTypes().get(0);
diff --git a/proto-google-cloud-storage-v2/src/main/proto/google/storage/v2/storage.proto b/proto-google-cloud-storage-v2/src/main/proto/google/storage/v2/storage.proto
index de6e27ea..a8516a58 100644
--- a/proto-google-cloud-storage-v2/src/main/proto/google/storage/v2/storage.proto
+++ b/proto-google-cloud-storage-v2/src/main/proto/google/storage/v2/storage.proto
@@ -2458,6 +2458,20 @@ message Object {
 
   // A user-specified timestamp set on an object.
   google.protobuf.Timestamp custom_time = 26;
+
+  // Output only. This is the time when the object became soft-deleted.
+  //
+  // Soft-deleted objects are only accessible if a soft_delete_policy is
+  // enabled. Also see hard_delete_time.
+  optional google.protobuf.Timestamp soft_delete_time = 28
+      [(google.api.field_behavior) = OUTPUT_ONLY];
+
+  // Output only. The time when the object will be permanently deleted.
+  //
+  // Only set when an object becomes soft-deleted with a soft_delete_policy.
+  // Otherwise, the object will not be accessible.
+  optional google.protobuf.Timestamp hard_delete_time = 29
+      [(google.api.field_behavior) = OUTPUT_ONLY];
 }
 
 // An access-control entry.
diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml
index f93fbd2f..4b6b9313 100644
--- a/samples/install-without-bom/pom.xml
+++ b/samples/install-without-bom/pom.xml
@@ -30,7 +30,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-storage</artifactId>
-      <version>2.33.0</version>
+      <version>2.34.0</version>
     </dependency>
     <!-- [END storage_install_without_bom] -->
 
@@ -43,7 +43,7 @@
     <dependency>
       <groupId>com.google.truth</groupId>
       <artifactId>truth</artifactId>
-      <version>1.4.0</version>
+      <version>1.4.1</version>
       <scope>test</scope>
     </dependency>
       <dependency>
diff --git a/samples/native-image-sample/pom.xml b/samples/native-image-sample/pom.xml
index 9ed2f969..b1259a5b 100644
--- a/samples/native-image-sample/pom.xml
+++ b/samples/native-image-sample/pom.xml
@@ -29,7 +29,7 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>libraries-bom</artifactId>
-        <version>26.32.0</version>
+        <version>26.33.0</version>
         <type>pom</type>
         <scope>import</scope>
       </dependency>
@@ -55,7 +55,7 @@
     <dependency>
       <groupId>com.google.truth</groupId>
       <artifactId>truth</artifactId>
-      <version>1.4.0</version>
+      <version>1.4.1</version>
       <scope>test</scope>
     </dependency>
     <dependency>
diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml
index 33f142aa..2c845a10 100644
--- a/samples/snapshot/pom.xml
+++ b/samples/snapshot/pom.xml
@@ -40,7 +40,7 @@
     <dependency>
       <groupId>com.google.truth</groupId>
       <artifactId>truth</artifactId>
-      <version>1.4.0</version>
+      <version>1.4.1</version>
       <scope>test</scope>
     </dependency>
     <dependency>
diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml
index 997f6784..007949d9 100644
--- a/samples/snippets/pom.xml
+++ b/samples/snippets/pom.xml
@@ -31,7 +31,7 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>libraries-bom</artifactId>
-        <version>26.32.0</version>
+        <version>26.33.0</version>
         <type>pom</type>
         <scope>import</scope>
       </dependency>
@@ -54,7 +54,7 @@
     <dependency>
       <groupId>com.google.truth</groupId>
       <artifactId>truth</artifactId>
-      <version>1.4.0</version>
+      <version>1.4.1</version>
       <scope>test</scope>
     </dependency>
     <dependency>
diff --git a/samples/snippets/src/main/java/com/example/storage/bucket/CreateBucketWithObjectRetention.java b/samples/snippets/src/main/java/com/example/storage/bucket/CreateBucketWithObjectRetention.java
new file mode 100644
index 00000000..5854a6da
--- /dev/null
+++ b/samples/snippets/src/main/java/com/example/storage/bucket/CreateBucketWithObjectRetention.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2024 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.example.storage.bucket;
+
+// [START storage_create_bucket_with_object_retention]
+
+import com.google.cloud.storage.Bucket;
+import com.google.cloud.storage.BucketInfo;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.StorageOptions;
+
+public class CreateBucketWithObjectRetention {
+  public static void createBucketWithObjectRetention(String projectId, String bucketName) {
+    // The ID of your GCP project
+    // String projectId = "your-project-id";
+
+    // The ID to give your GCS bucket
+    // String bucketName = "your-unique-bucket-name";
+
+    Storage storage = StorageOptions.newBuilder().setProjectId(projectId).build().getService();
+
+    Bucket bucket =
+        storage.create(
+            BucketInfo.of(bucketName), Storage.BucketTargetOption.enableObjectRetention(true));
+
+    System.out.println(
+        "Created bucket "
+            + bucket.getName()
+            + " with object retention enabled setting: "
+            + bucket.getObjectRetention().getMode().toString());
+  }
+}
+
+// [END storage_create_bucket_with_object_retention]
diff --git a/samples/snippets/src/main/java/com/example/storage/bucket/GetBucketMetadata.java b/samples/snippets/src/main/java/com/example/storage/bucket/GetBucketMetadata.java
index 95dd5c41..742120f8 100644
--- a/samples/snippets/src/main/java/com/example/storage/bucket/GetBucketMetadata.java
+++ b/samples/snippets/src/main/java/com/example/storage/bucket/GetBucketMetadata.java
@@ -56,6 +56,7 @@ public class GetBucketMetadata {
     System.out.println("StorageClass: " + bucket.getStorageClass().name());
     System.out.println("TimeCreated: " + bucket.getCreateTime());
     System.out.println("VersioningEnabled: " + bucket.versioningEnabled());
+    System.out.println("ObjectRetention: " + bucket.getObjectRetention());
     if (bucket.getLabels() != null) {
       System.out.println("\n\n\nLabels:");
       for (Map.Entry<String, String> label : bucket.getLabels().entrySet()) {
diff --git a/samples/snippets/src/main/java/com/example/storage/object/GetObjectMetadata.java b/samples/snippets/src/main/java/com/example/storage/object/GetObjectMetadata.java
index 74e452c3..4b6eb42e 100644
--- a/samples/snippets/src/main/java/com/example/storage/object/GetObjectMetadata.java
+++ b/samples/snippets/src/main/java/com/example/storage/object/GetObjectMetadata.java
@@ -68,6 +68,7 @@ public class GetObjectMetadata {
     System.out.println("StorageClass: " + blob.getStorageClass());
     System.out.println("TimeCreated: " + new Date(blob.getCreateTime()));
     System.out.println("Last Metadata Update: " + new Date(blob.getUpdateTime()));
+    System.out.println("Object Retention Policy: " + blob.getRetention());
     Boolean temporaryHoldIsEnabled = (blob.getTemporaryHold() != null && blob.getTemporaryHold());
     System.out.println("temporaryHold: " + (temporaryHoldIsEnabled ? "enabled" : "disabled"));
     Boolean eventBasedHoldIsEnabled =
diff --git a/samples/snippets/src/main/java/com/example/storage/object/SetObjectRetentionPolicy.java b/samples/snippets/src/main/java/com/example/storage/object/SetObjectRetentionPolicy.java
new file mode 100644
index 00000000..6903445a
--- /dev/null
+++ b/samples/snippets/src/main/java/com/example/storage/object/SetObjectRetentionPolicy.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2022 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.example.storage.object;
+
+// [START storage_set_object_retention_policy]
+
+import static java.time.OffsetDateTime.now;
+
+import com.google.cloud.storage.Blob;
+import com.google.cloud.storage.BlobId;
+import com.google.cloud.storage.BlobInfo.Retention;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.StorageException;
+import com.google.cloud.storage.StorageOptions;
+
+public class SetObjectRetentionPolicy {
+  public static void setObjectRetentionPolicy(
+      String projectId, String bucketName, String objectName) throws StorageException {
+    // The ID of your GCP project
+    // String projectId = "your-project-id";
+
+    // The ID of your GCS bucket that has object retention enabled
+    // String bucketName = "your-unique-bucket-name";
+
+    // The ID of your GCS object
+    // String objectName = "your-object-name";
+
+    Storage storage = StorageOptions.newBuilder().setProjectId(projectId).build().getService();
+    BlobId blobId = BlobId.of(bucketName, objectName);
+    Blob blob = storage.get(blobId);
+    if (blob == null) {
+      System.out.println("The object " + objectName + " was not found in " + bucketName);
+      return;
+    }
+
+    Blob updated =
+        blob.toBuilder()
+            .setRetention(
+                Retention.newBuilder()
+                    .setMode(Retention.Mode.UNLOCKED)
+                    .setRetainUntilTime(now().plusDays(10))
+                    .build())
+            .build()
+            .update();
+
+    System.out.println("Retention policy for object " + objectName + " was set to:");
+    System.out.println(updated.getRetention().toString());
+
+    // To modify an existing policy on an Unlocked object, pass in the override parameter
+    blob.toBuilder()
+        .setRetention(
+            updated.getRetention().toBuilder().setRetainUntilTime(now().plusDays(9)).build())
+        .build()
+        .update(Storage.BlobTargetOption.overrideUnlockedRetention(true));
+
+    System.out.println("Retention policy for object " + objectName + " was updated to:");
+    System.out.println(storage.get(blobId).getRetention().toString());
+  }
+}
+
+// [END storage_set_object_retention_policy]
diff --git a/storage-shared-benchmarking/src/main/java/com/google/cloud/storage/benchmarking/Bidi.java b/storage-shared-benchmarking/src/main/java/com/google/cloud/storage/benchmarking/Bidi.java
new file mode 100644
index 00000000..aa59e0b3
--- /dev/null
+++ b/storage-shared-benchmarking/src/main/java/com/google/cloud/storage/benchmarking/Bidi.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2024 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
+ *
+ *      https://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.benchmarking;
+
+import static com.google.cloud.storage.benchmarking.StorageSharedBenchmarkingUtils.generateCloudMonitoringResult;
+
+import com.google.cloud.storage.BlobInfo;
+import com.google.cloud.storage.BlobWriteSession;
+import com.google.cloud.storage.DataGenerator;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.Storage.BlobWriteOption;
+import java.io.PrintWriter;
+import java.nio.ByteBuffer;
+import java.nio.channels.WritableByteChannel;
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.concurrent.Callable;
+
+class Bidi implements Callable<String> {
+  private final Storage storageClient;
+  private final String bucketName;
+  private final int objectSize;
+  private final PrintWriter pw;
+  private final String api;
+  private final int workers;
+
+  Bidi(
+      Storage storageClient,
+      String bucketName,
+      int objectSize,
+      PrintWriter pw,
+      String api,
+      int workers) {
+    this.storageClient = storageClient;
+    this.bucketName = bucketName;
+    this.objectSize = objectSize;
+    this.pw = pw;
+    this.api = api;
+    this.workers = workers;
+  }
+
+  @Override
+  public String call() throws Exception {
+    String blobName = DataGenerator.base64Characters().genBytes(20).toString();
+    BlobWriteSession sess =
+        storageClient.blobWriteSession(
+            BlobInfo.newBuilder(bucketName, blobName).build(), BlobWriteOption.doesNotExist());
+    byte[] bytes = DataGenerator.base64Characters().genBytes(objectSize);
+    Clock clock = Clock.systemDefaultZone();
+    Instant startTime = clock.instant();
+    try (WritableByteChannel w = sess.open()) {
+      w.write(ByteBuffer.wrap(bytes));
+    }
+    BlobInfo created = sess.getResult().get();
+    Instant endTime = clock.instant();
+    Duration elapsedTimeWrite = Duration.between(startTime, endTime);
+    printResult("BIDI", created, elapsedTimeWrite);
+    StorageSharedBenchmarkingUtils.cleanupObject(storageClient, created);
+    return "OK";
+  }
+
+  private void printResult(String op, BlobInfo created, Duration duration) {
+    pw.println(
+        generateCloudMonitoringResult(
+                op,
+                StorageSharedBenchmarkingUtils.calculateThroughput(
+                    created.getSize().doubleValue(), duration),
+                created,
+                api,
+                workers)
+            .formatAsCustomMetric());
+  }
+}
diff --git a/storage-shared-benchmarking/src/main/java/com/google/cloud/storage/benchmarking/StorageSharedBenchmarkingCli.java b/storage-shared-benchmarking/src/main/java/com/google/cloud/storage/benchmarking/StorageSharedBenchmarkingCli.java
index e6c4c056..836c28d0 100644
--- a/storage-shared-benchmarking/src/main/java/com/google/cloud/storage/benchmarking/StorageSharedBenchmarkingCli.java
+++ b/storage-shared-benchmarking/src/main/java/com/google/cloud/storage/benchmarking/StorageSharedBenchmarkingCli.java
@@ -19,6 +19,7 @@ package com.google.cloud.storage.benchmarking;
 import com.google.api.core.ApiFuture;
 import com.google.api.core.ListenableFutureToApiFuture;
 import com.google.api.gax.retrying.RetrySettings;
+import com.google.cloud.storage.BlobWriteSessionConfigs;
 import com.google.cloud.storage.Storage;
 import com.google.cloud.storage.StorageOptions;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -106,6 +107,12 @@ public final class StorageSharedBenchmarkingCli implements Runnable {
       case "w1r3":
         runWorkload1();
         break;
+      case "bidi":
+        runWorkloadBidi();
+        break;
+      case "default-nobidi":
+        runWorkloadNoBidi();
+        break;
       default:
         throw new IllegalStateException("Specify a workload to run");
     }
@@ -151,6 +158,36 @@ public final class StorageSharedBenchmarkingCli implements Runnable {
     }
   }
 
+  private void runWorkloadBidi() {
+    StorageOptions options =
+        StorageOptions.grpc()
+            .setProjectId(project)
+            .setBlobWriteSessionConfig(BlobWriteSessionConfigs.bidiWrite())
+            .build();
+    Storage storageClient = options.getService();
+    try {
+      runBidi(storageClient);
+    } catch (Exception e) {
+      System.err.println("Failed to run workload bidi" + e.getMessage());
+      System.exit(1);
+    }
+  }
+
+  private void runWorkloadNoBidi() {
+    StorageOptions options =
+        StorageOptions.grpc()
+            .setProjectId(project)
+            .setBlobWriteSessionConfig(BlobWriteSessionConfigs.getDefault())
+            .build();
+    Storage storageClient = options.getService();
+    try {
+      runBidi(storageClient);
+    } catch (Exception e) {
+      System.err.println("Failed to run workload no bidi" + e.getMessage());
+      System.exit(1);
+    }
+  }
+
   private void runW1R3(Storage storageClient) throws ExecutionException, InterruptedException {
     ListeningExecutorService executorService =
         MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(workers));
@@ -173,6 +210,19 @@ public final class StorageSharedBenchmarkingCli implements Runnable {
     }
   }
 
+  private void runBidi(Storage storageClient) throws ExecutionException, InterruptedException {
+    ListeningExecutorService executorService =
+        MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(workers));
+    for (int i = 0; i < samples; i++) {
+      Range objectSizeRange = Range.of(objectSize);
+      int objectSize = getRandomInt(objectSizeRange.min, objectSizeRange.max);
+      convert(
+              executorService.submit(
+                  new Bidi(storageClient, bucket, objectSize, printWriter, api, workers)))
+          .get();
+    }
+  }
+
   private void runWarmup(Storage storageClient) throws ExecutionException, InterruptedException {
     if (warmup <= 0) {
       return;
diff --git a/storage-shared-benchmarking/src/main/java/com/google/cloud/storage/benchmarking/StorageSharedBenchmarkingUtils.java b/storage-shared-benchmarking/src/main/java/com/google/cloud/storage/benchmarking/StorageSharedBenchmarkingUtils.java
index 137fee51..52420723 100644
--- a/storage-shared-benchmarking/src/main/java/com/google/cloud/storage/benchmarking/StorageSharedBenchmarkingUtils.java
+++ b/storage-shared-benchmarking/src/main/java/com/google/cloud/storage/benchmarking/StorageSharedBenchmarkingUtils.java
@@ -15,7 +15,7 @@
  */
 package com.google.cloud.storage.benchmarking;
 
-import com.google.cloud.storage.Blob;
+import com.google.cloud.storage.BlobInfo;
 import com.google.cloud.storage.Storage;
 import java.time.Duration;
 
@@ -23,7 +23,7 @@ class StorageSharedBenchmarkingUtils {
   public static long SSB_SIZE_THRESHOLD_BYTES = 1048576;
   public static int DEFAULT_NUMBER_OF_READS = 3;
 
-  public static void cleanupObject(Storage storage, Blob created) {
+  public static void cleanupObject(Storage storage, BlobInfo created) {
     storage.delete(
         created.getBlobId(), Storage.BlobSourceOption.generationMatch(created.getGeneration()));
   }
@@ -36,4 +36,25 @@ class StorageSharedBenchmarkingUtils {
     double throughput = adjustedSize / (elapsedTime.toMillis() / 1000D);
     return throughput;
   }
+
+  public static CloudMonitoringResult generateCloudMonitoringResult(
+      String op, double throughput, BlobInfo created, String api, int workers) {
+    CloudMonitoringResult result =
+        CloudMonitoringResult.newBuilder()
+            .setLibrary("java")
+            .setApi(api)
+            .setOp(op)
+            .setWorkers(workers)
+            .setObjectSize(created.getSize().intValue())
+            .setChunksize(created.getSize().intValue())
+            .setCrc32cEnabled(false)
+            .setMd5Enabled(false)
+            .setCpuTimeUs(-1)
+            .setBucketName(created.getBucket())
+            .setStatus("OK")
+            .setTransferSize(created.getSize().toString())
+            .setThroughput(throughput)
+            .build();
+    return result;
+  }
 }
diff --git a/storage-shared-benchmarking/src/main/java/com/google/cloud/storage/benchmarking/W1R3.java b/storage-shared-benchmarking/src/main/java/com/google/cloud/storage/benchmarking/W1R3.java
index 83b357db..7405d9cf 100644
--- a/storage-shared-benchmarking/src/main/java/com/google/cloud/storage/benchmarking/W1R3.java
+++ b/storage-shared-benchmarking/src/main/java/com/google/cloud/storage/benchmarking/W1R3.java
@@ -16,6 +16,8 @@
 
 package com.google.cloud.storage.benchmarking;
 
+import static com.google.cloud.storage.benchmarking.StorageSharedBenchmarkingUtils.generateCloudMonitoringResult;
+
 import com.google.cloud.storage.Blob;
 import com.google.cloud.storage.BlobInfo;
 import com.google.cloud.storage.DataGenerator;
@@ -80,7 +82,7 @@ final class W1R3 implements Callable<String> {
           printResult("READ[" + i + "]", created, elapsedTimeDownload);
         }
       }
-      StorageSharedBenchmarkingUtils.cleanupObject(storage, created);
+      StorageSharedBenchmarkingUtils.cleanupObject(storage, created.asBlobInfo());
     } catch (Exception e) {
       CloudMonitoringResult result =
           CloudMonitoringResult.newBuilder()
@@ -110,29 +112,10 @@ final class W1R3 implements Callable<String> {
                   op,
                   StorageSharedBenchmarkingUtils.calculateThroughput(
                       created.getSize().doubleValue(), duration),
-                  created)
+                  created.asBlobInfo(),
+                  api,
+                  workers)
               .formatAsCustomMetric());
     }
   }
-
-  private CloudMonitoringResult generateCloudMonitoringResult(
-      String op, double throughput, Blob created) {
-    CloudMonitoringResult result =
-        CloudMonitoringResult.newBuilder()
-            .setLibrary("java")
-            .setApi(api)
-            .setOp(op)
-            .setWorkers(workers)
-            .setObjectSize(created.getSize().intValue())
-            .setChunksize(created.getSize().intValue())
-            .setCrc32cEnabled(false)
-            .setMd5Enabled(false)
-            .setCpuTimeUs(-1)
-            .setBucketName(created.getBucket())
-            .setStatus("OK")
-            .setTransferSize(created.getSize().toString())
-            .setThroughput(throughput)
-            .build();
-    return result;
-  }
 }


Test patch:
diff --git a/gapic-google-cloud-storage-v2/src/test/java/com/google/storage/v2/MockStorage.java b/gapic-google-cloud-storage-v2/src/test/java/com/google/storage/v2/MockStorage.java
index 5f04f6e6..3bee4c25 100644
--- a/gapic-google-cloud-storage-v2/src/test/java/com/google/storage/v2/MockStorage.java
+++ b/gapic-google-cloud-storage-v2/src/test/java/com/google/storage/v2/MockStorage.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/gapic-google-cloud-storage-v2/src/test/java/com/google/storage/v2/MockStorageImpl.java b/gapic-google-cloud-storage-v2/src/test/java/com/google/storage/v2/MockStorageImpl.java
index 33da2cdb..3d39e9d2 100644
--- a/gapic-google-cloud-storage-v2/src/test/java/com/google/storage/v2/MockStorageImpl.java
+++ b/gapic-google-cloud-storage-v2/src/test/java/com/google/storage/v2/MockStorageImpl.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/gapic-google-cloud-storage-v2/src/test/java/com/google/storage/v2/StorageClientTest.java b/gapic-google-cloud-storage-v2/src/test/java/com/google/storage/v2/StorageClientTest.java
index b9a9d14f..82a4c7b9 100644
--- a/gapic-google-cloud-storage-v2/src/test/java/com/google/storage/v2/StorageClientTest.java
+++ b/gapic-google-cloud-storage-v2/src/test/java/com/google/storage/v2/StorageClientTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -1382,6 +1382,8 @@ public class StorageClientTest {
             .setOwner(Owner.newBuilder().build())
             .setCustomerEncryption(CustomerEncryption.newBuilder().build())
             .setCustomTime(Timestamp.newBuilder().build())
+            .setSoftDeleteTime(Timestamp.newBuilder().build())
+            .setHardDeleteTime(Timestamp.newBuilder().build())
             .build();
     mockStorage.addResponse(expectedResponse);
 
@@ -1636,6 +1638,8 @@ public class StorageClientTest {
             .setOwner(Owner.newBuilder().build())
             .setCustomerEncryption(CustomerEncryption.newBuilder().build())
             .setCustomTime(Timestamp.newBuilder().build())
+            .setSoftDeleteTime(Timestamp.newBuilder().build())
+            .setHardDeleteTime(Timestamp.newBuilder().build())
             .build();
     mockStorage.addResponse(expectedResponse);
 
@@ -1708,6 +1712,8 @@ public class StorageClientTest {
             .setOwner(Owner.newBuilder().build())
             .setCustomerEncryption(CustomerEncryption.newBuilder().build())
             .setCustomTime(Timestamp.newBuilder().build())
+            .setSoftDeleteTime(Timestamp.newBuilder().build())
+            .setHardDeleteTime(Timestamp.newBuilder().build())
             .build();
     mockStorage.addResponse(expectedResponse);
 
@@ -1817,6 +1823,8 @@ public class StorageClientTest {
             .setOwner(Owner.newBuilder().build())
             .setCustomerEncryption(CustomerEncryption.newBuilder().build())
             .setCustomTime(Timestamp.newBuilder().build())
+            .setSoftDeleteTime(Timestamp.newBuilder().build())
+            .setHardDeleteTime(Timestamp.newBuilder().build())
             .build();
     mockStorage.addResponse(expectedResponse);
 
@@ -1886,6 +1894,8 @@ public class StorageClientTest {
             .setOwner(Owner.newBuilder().build())
             .setCustomerEncryption(CustomerEncryption.newBuilder().build())
             .setCustomTime(Timestamp.newBuilder().build())
+            .setSoftDeleteTime(Timestamp.newBuilder().build())
+            .setHardDeleteTime(Timestamp.newBuilder().build())
             .build();
     mockStorage.addResponse(expectedResponse);
 
@@ -1955,6 +1965,8 @@ public class StorageClientTest {
             .setOwner(Owner.newBuilder().build())
             .setCustomerEncryption(CustomerEncryption.newBuilder().build())
             .setCustomTime(Timestamp.newBuilder().build())
+            .setSoftDeleteTime(Timestamp.newBuilder().build())
+            .setHardDeleteTime(Timestamp.newBuilder().build())
             .build();
     mockStorage.addResponse(expectedResponse);
 
@@ -2027,6 +2039,8 @@ public class StorageClientTest {
             .setOwner(Owner.newBuilder().build())
             .setCustomerEncryption(CustomerEncryption.newBuilder().build())
             .setCustomTime(Timestamp.newBuilder().build())
+            .setSoftDeleteTime(Timestamp.newBuilder().build())
+            .setHardDeleteTime(Timestamp.newBuilder().build())
             .build();
     mockStorage.addResponse(expectedResponse);
 
@@ -2170,6 +2184,8 @@ public class StorageClientTest {
             .setOwner(Owner.newBuilder().build())
             .setCustomerEncryption(CustomerEncryption.newBuilder().build())
             .setCustomTime(Timestamp.newBuilder().build())
+            .setSoftDeleteTime(Timestamp.newBuilder().build())
+            .setHardDeleteTime(Timestamp.newBuilder().build())
             .build();
     mockStorage.addResponse(expectedResponse);
 
diff --git a/google-cloud-storage-control/src/test/java/com/google/storage/control/v2/MockStorageControl.java b/google-cloud-storage-control/src/test/java/com/google/storage/control/v2/MockStorageControl.java
index 0f8d5ed3..6dbac59d 100644
--- a/google-cloud-storage-control/src/test/java/com/google/storage/control/v2/MockStorageControl.java
+++ b/google-cloud-storage-control/src/test/java/com/google/storage/control/v2/MockStorageControl.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/google-cloud-storage-control/src/test/java/com/google/storage/control/v2/MockStorageControlImpl.java b/google-cloud-storage-control/src/test/java/com/google/storage/control/v2/MockStorageControlImpl.java
index b2f2c078..0177422a 100644
--- a/google-cloud-storage-control/src/test/java/com/google/storage/control/v2/MockStorageControlImpl.java
+++ b/google-cloud-storage-control/src/test/java/com/google/storage/control/v2/MockStorageControlImpl.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/google-cloud-storage-control/src/test/java/com/google/storage/control/v2/StorageControlClientTest.java b/google-cloud-storage-control/src/test/java/com/google/storage/control/v2/StorageControlClientTest.java
index b29a26d8..8559283b 100644
--- a/google-cloud-storage-control/src/test/java/com/google/storage/control/v2/StorageControlClientTest.java
+++ b/google-cloud-storage-control/src/test/java/com/google/storage/control/v2/StorageControlClientTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 Google LLC
+ * Copyright 2024 Google LLC
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobWriteSessionTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobWriteSessionTest.java
index bf8132b3..763346b1 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobWriteSessionTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobWriteSessionTest.java
@@ -102,6 +102,23 @@ public final class ITBlobWriteSessionTest {
     }
   }
 
+  @Test
+  public void bidiTest() throws Exception {
+    StorageOptions options = null;
+    if (transport == Transport.GRPC) {
+      options =
+          ((GrpcStorageOptions) storage.getOptions())
+              .toBuilder()
+              .setBlobWriteSessionConfig(BlobWriteSessionConfigs.bidiWrite())
+              .build();
+    }
+    assertWithMessage("unable to resolve options").that(options).isNotNull();
+
+    try (Storage s = options.getService()) {
+      doTest(s);
+    }
+  }
+
   @Test
   public void closingAnOpenedSessionWithoutCallingWriteShouldMakeAnEmptyObject()
       throws IOException, ExecutionException, InterruptedException, TimeoutException {
diff --git a/samples/snippets/src/test/java/com/example/storage/ITBucketSnippets.java b/samples/snippets/src/test/java/com/example/storage/ITBucketSnippets.java
index 5bc17937..2f61fdbb 100644
--- a/samples/snippets/src/test/java/com/example/storage/ITBucketSnippets.java
+++ b/samples/snippets/src/test/java/com/example/storage/ITBucketSnippets.java
@@ -30,6 +30,7 @@ import com.example.storage.bucket.AddBucketLabel;
 import com.example.storage.bucket.ChangeDefaultStorageClass;
 import com.example.storage.bucket.ConfigureBucketCors;
 import com.example.storage.bucket.CreateBucket;
+import com.example.storage.bucket.CreateBucketWithObjectRetention;
 import com.example.storage.bucket.CreateBucketWithStorageClassAndLocation;
 import com.example.storage.bucket.CreateBucketWithTurboReplication;
 import com.example.storage.bucket.DeleteBucket;
@@ -265,6 +266,7 @@ public class ITBucketSnippets {
     assertTrue(snippetOutput.contains("Labels:"));
     assertTrue(snippetOutput.contains("k=v"));
     assertTrue(snippetOutput.contains("Lifecycle Rules:"));
+    assertTrue(snippetOutput.contains("ObjectRetention: " + bucket.getObjectRetention()));
   }
 
   @Test
@@ -662,10 +664,14 @@ public class ITBucketSnippets {
     String tempBucket = RemoteStorageHelper.generateBucketName();
     Bucket bucket = storage.create(BucketInfo.of(tempBucket));
     assertNotNull(bucket);
-    SetRetentionPolicy.setRetentionPolicy(PROJECT_ID, tempBucket, 5L);
-    assertEquals(5L, (long) storage.get(tempBucket).getRetentionPeriod());
-    LockRetentionPolicy.lockRetentionPolicy(PROJECT_ID, tempBucket);
-    assertTrue(storage.get(tempBucket).retentionPolicyIsLocked());
+    try {
+      SetRetentionPolicy.setRetentionPolicy(PROJECT_ID, tempBucket, 5L);
+      assertEquals(5L, (long) storage.get(tempBucket).getRetentionPeriod());
+      LockRetentionPolicy.lockRetentionPolicy(PROJECT_ID, tempBucket);
+      assertTrue(storage.get(tempBucket).retentionPolicyIsLocked());
+    } finally {
+      storage.delete(tempBucket);
+    }
   }
 
   @Test
@@ -682,4 +688,18 @@ public class ITBucketSnippets {
     DisableUniformBucketLevelAccess.disableUniformBucketLevelAccess(PROJECT_ID, BUCKET);
     assertFalse(storage.get(BUCKET).getIamConfiguration().isUniformBucketLevelAccessEnabled());
   }
+
+  @Test
+  public void testCreateBucketWithObjectRetention() {
+    String tempBucket = RemoteStorageHelper.generateBucketName();
+
+    try {
+      CreateBucketWithObjectRetention.createBucketWithObjectRetention(PROJECT_ID, tempBucket);
+      assertNotNull(storage.get(tempBucket).getObjectRetention());
+      String snippetOutput = stdOutCaptureRule.getCapturedOutputAsUtf8String();
+      assertTrue(snippetOutput.contains("Enabled"));
+    } finally {
+      storage.delete(tempBucket);
+    }
+  }
 }
diff --git a/samples/snippets/src/test/java/com/example/storage/ITObjectSnippets.java b/samples/snippets/src/test/java/com/example/storage/ITObjectSnippets.java
index 7d7b69d4..feca2ca8 100644
--- a/samples/snippets/src/test/java/com/example/storage/ITObjectSnippets.java
+++ b/samples/snippets/src/test/java/com/example/storage/ITObjectSnippets.java
@@ -47,6 +47,7 @@ import com.example.storage.object.MakeObjectPublic;
 import com.example.storage.object.MoveObject;
 import com.example.storage.object.RotateObjectEncryptionKey;
 import com.example.storage.object.SetObjectMetadata;
+import com.example.storage.object.SetObjectRetentionPolicy;
 import com.example.storage.object.StreamObjectDownload;
 import com.example.storage.object.StreamObjectUpload;
 import com.example.storage.object.UploadEncryptedObject;
@@ -229,6 +230,7 @@ public class ITObjectSnippets {
     assertTrue(snippetOutput.contains("eventBasedHold: disabled"));
     assertTrue(snippetOutput.contains("User metadata:"));
     assertTrue(snippetOutput.contains("k=v"));
+    assertTrue(snippetOutput.contains("Object Retention Policy: " + remoteBlob.getRetention()));
   }
 
   @Test
@@ -434,4 +436,28 @@ public class ITObjectSnippets {
     assertEquals("value", firstBlobMetadata.get("keyToAddOrUpdate"));
     assertEquals("value", secondBlobMetadata.get("keyToAddOrUpdate"));
   }
+
+  @Test
+  public void testSetObjectRetentionPolicy() {
+    String tempBucket = RemoteStorageHelper.generateBucketName();
+    storage.create(
+        BucketInfo.of(tempBucket), Storage.BucketTargetOption.enableObjectRetention(true));
+    String retentionBlob = "retentionblob";
+    storage.create(BlobInfo.newBuilder(tempBucket, retentionBlob).build());
+    assertNull(storage.get(tempBucket, retentionBlob).getRetention());
+    try {
+      SetObjectRetentionPolicy.setObjectRetentionPolicy(PROJECT_ID, tempBucket, retentionBlob);
+      assertNotNull(storage.get(tempBucket, retentionBlob).getRetention());
+    } finally {
+
+      storage
+          .get(tempBucket, retentionBlob)
+          .toBuilder()
+          .setRetention(null)
+          .build()
+          .update(Storage.BlobTargetOption.overrideUnlockedRetention(true));
+      storage.delete(tempBucket, retentionBlob);
+      storage.delete(tempBucket);
+    }
+  }
 }