[google_sign_in] Implement `disconnect` for Android (#9991)

Adds the missing implementation of `disconnect` using the new `revokeAccess` API, updating `play-services-auth` to the version containing the new API.

Fixes https://github.com/flutter/flutter/issues/169612

## Pre-Review Checklist

[^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
diff --git a/packages/google_sign_in/google_sign_in_android/CHANGELOG.md b/packages/google_sign_in/google_sign_in_android/CHANGELOG.md
index eabc8cb..bbabd96 100644
--- a/packages/google_sign_in/google_sign_in_android/CHANGELOG.md
+++ b/packages/google_sign_in/google_sign_in_android/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 7.2.0
+
+* Adds support for `disconnect`.
+
 ## 7.1.0
 
 * Adds support for the `clearAuthorizationToken` method.
diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java
index 7242103..1c3b8ab 100644
--- a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java
+++ b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java
@@ -34,6 +34,7 @@
 import com.google.android.gms.auth.api.identity.AuthorizationResult;
 import com.google.android.gms.auth.api.identity.ClearTokenRequest;
 import com.google.android.gms.auth.api.identity.Identity;
+import com.google.android.gms.auth.api.identity.RevokeAccessRequest;
 import com.google.android.gms.common.api.ApiException;
 import com.google.android.gms.common.api.Scope;
 import com.google.android.libraries.identity.googleid.GetGoogleIdOption;
@@ -59,6 +60,9 @@
   private @Nullable BinaryMessenger messenger;
   private ActivityPluginBinding activityPluginBinding;
 
+  // The account type to use to create an Account object for a Google Sign In account.
+  private static final String GOOGLE_ACCOUNT_TYPE = "com.google";
+
   private void initInstance(@NonNull BinaryMessenger messenger, @NonNull Context context) {
     initWithDelegate(
         messenger,
@@ -384,7 +388,7 @@
         }
         if (params.getAccountEmail() != null) {
           authorizationRequestBuilder.setAccount(
-              new Account(params.getAccountEmail(), "com.google"));
+              new Account(params.getAccountEmail(), GOOGLE_ACCOUNT_TYPE));
         }
         AuthorizationRequest authorizationRequest = authorizationRequestBuilder.build();
         authorizationClientFactory
@@ -456,6 +460,28 @@
     }
 
     @Override
+    public void revokeAccess(
+        @NonNull PlatformRevokeAccessRequest params,
+        @NonNull Function1<? super Result<Unit>, Unit> callback) {
+      List<Scope> scopes = new ArrayList<>();
+      for (String scope : params.getScopes()) {
+        scopes.add(new Scope(scope));
+      }
+      authorizationClientFactory
+          .create(context)
+          .revokeAccess(
+              RevokeAccessRequest.builder()
+                  .setAccount(new Account(params.getAccountEmail(), GOOGLE_ACCOUNT_TYPE))
+                  .setScopes(scopes)
+                  .build())
+          .addOnSuccessListener(unused -> ResultUtilsKt.completeWithUnitSuccess(callback))
+          .addOnFailureListener(
+              e ->
+                  ResultUtilsKt.completeWithUnitError(
+                      callback, new FlutterError("revokeAccess failed", e.getMessage(), null)));
+    }
+
+    @Override
     public boolean onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
       if (requestCode == REQUEST_CODE_AUTHORIZE) {
         if (pendingAuthorizationCallback != null) {
diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt b/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt
index 149c72e..9f446f2 100644
--- a/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt
+++ b/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt
@@ -276,6 +276,53 @@
 }
 
 /**
+ * Parameters for revoking authorization.
+ *
+ * Corresponds to the native RevokeAccessRequest.
+ * https://developers.google.com/android/reference/com/google/android/gms/auth/api/identity/RevokeAccessRequest
+ *
+ * Generated class from Pigeon that represents data sent in messages.
+ */
+data class PlatformRevokeAccessRequest(
+    /** The email for the Google account to revoke authorizations for. */
+    val accountEmail: String,
+    /**
+     * A list of requested scopes.
+     *
+     * Per docs, all granted scopes will be revoked, not only the ones passed here. However, at
+     * least one currently-granted scope must be provided.
+     */
+    val scopes: List<String>
+) {
+  companion object {
+    fun fromList(pigeonVar_list: List<Any?>): PlatformRevokeAccessRequest {
+      val accountEmail = pigeonVar_list[0] as String
+      val scopes = pigeonVar_list[1] as List<String>
+      return PlatformRevokeAccessRequest(accountEmail, scopes)
+    }
+  }
+
+  fun toList(): List<Any?> {
+    return listOf(
+        accountEmail,
+        scopes,
+    )
+  }
+
+  override fun equals(other: Any?): Boolean {
+    if (other !is PlatformRevokeAccessRequest) {
+      return false
+    }
+    if (this === other) {
+      return true
+    }
+    return MessagesPigeonUtils.deepEquals(toList(), other.toList())
+  }
+
+  override fun hashCode(): Int = toList().hashCode()
+}
+
+/**
  * Pigeon equivalent of the native GoogleIdTokenCredential.
  *
  * Generated class from Pigeon that represents data sent in messages.
@@ -525,20 +572,23 @@
         }
       }
       134.toByte() -> {
+        return (readValue(buffer) as? List<Any?>)?.let { PlatformRevokeAccessRequest.fromList(it) }
+      }
+      135.toByte() -> {
         return (readValue(buffer) as? List<Any?>)?.let {
           PlatformGoogleIdTokenCredential.fromList(it)
         }
       }
-      135.toByte() -> {
+      136.toByte() -> {
         return (readValue(buffer) as? List<Any?>)?.let { GetCredentialFailure.fromList(it) }
       }
-      136.toByte() -> {
+      137.toByte() -> {
         return (readValue(buffer) as? List<Any?>)?.let { GetCredentialSuccess.fromList(it) }
       }
-      137.toByte() -> {
+      138.toByte() -> {
         return (readValue(buffer) as? List<Any?>)?.let { AuthorizeFailure.fromList(it) }
       }
-      138.toByte() -> {
+      139.toByte() -> {
         return (readValue(buffer) as? List<Any?>)?.let { PlatformAuthorizationResult.fromList(it) }
       }
       else -> super.readValueOfType(type, buffer)
@@ -567,26 +617,30 @@
         stream.write(133)
         writeValue(stream, value.toList())
       }
-      is PlatformGoogleIdTokenCredential -> {
+      is PlatformRevokeAccessRequest -> {
         stream.write(134)
         writeValue(stream, value.toList())
       }
-      is GetCredentialFailure -> {
+      is PlatformGoogleIdTokenCredential -> {
         stream.write(135)
         writeValue(stream, value.toList())
       }
-      is GetCredentialSuccess -> {
+      is GetCredentialFailure -> {
         stream.write(136)
         writeValue(stream, value.toList())
       }
-      is AuthorizeFailure -> {
+      is GetCredentialSuccess -> {
         stream.write(137)
         writeValue(stream, value.toList())
       }
-      is PlatformAuthorizationResult -> {
+      is AuthorizeFailure -> {
         stream.write(138)
         writeValue(stream, value.toList())
       }
+      is PlatformAuthorizationResult -> {
+        stream.write(139)
+        writeValue(stream, value.toList())
+      }
       else -> super.writeValue(stream, value)
     }
   }
@@ -615,6 +669,8 @@
       callback: (Result<AuthorizeResult>) -> Unit
   )
 
+  fun revokeAccess(params: PlatformRevokeAccessRequest, callback: (Result<Unit>) -> Unit)
+
   companion object {
     /** The codec used by GoogleSignInApi. */
     val codec: MessageCodec<Any?> by lazy { MessagesPigeonCodec() }
@@ -742,6 +798,29 @@
           channel.setMessageHandler(null)
         }
       }
+      run {
+        val channel =
+            BasicMessageChannel<Any?>(
+                binaryMessenger,
+                "dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.revokeAccess$separatedMessageChannelSuffix",
+                codec)
+        if (api != null) {
+          channel.setMessageHandler { message, reply ->
+            val args = message as List<Any?>
+            val paramsArg = args[0] as PlatformRevokeAccessRequest
+            api.revokeAccess(paramsArg) { result: Result<Unit> ->
+              val error = result.exceptionOrNull()
+              if (error != null) {
+                reply.reply(MessagesPigeonUtils.wrapError(error))
+              } else {
+                reply.reply(MessagesPigeonUtils.wrapResult(null))
+              }
+            }
+          }
+        } else {
+          channel.setMessageHandler(null)
+        }
+      }
     }
   }
 }
diff --git a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java
index 7fa058c..e34bd3f 100644
--- a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java
+++ b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java
@@ -41,6 +41,7 @@
 import com.google.android.gms.auth.api.identity.AuthorizationRequest;
 import com.google.android.gms.auth.api.identity.AuthorizationResult;
 import com.google.android.gms.auth.api.identity.ClearTokenRequest;
+import com.google.android.gms.auth.api.identity.RevokeAccessRequest;
 import com.google.android.gms.common.api.ApiException;
 import com.google.android.gms.common.api.Status;
 import com.google.android.gms.tasks.OnSuccessListener;
@@ -72,7 +73,7 @@
   @Mock CustomCredential mockGenericCredential;
   @Mock GoogleIdTokenCredential mockGoogleCredential;
   @Mock Task<AuthorizationResult> mockAuthorizationTask;
-  @Mock Task<Void> mockClearTokenTask;
+  @Mock Task<Void> mockVoidTask;
 
   private GoogleSignInPlugin flutterPlugin;
   // Technically this is not the plugin, but in practice almost all of the functionality is in this
@@ -90,8 +91,8 @@
         .thenReturn(GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL);
     when(mockAuthorizationTask.addOnSuccessListener(any())).thenReturn(mockAuthorizationTask);
     when(mockAuthorizationTask.addOnFailureListener(any())).thenReturn(mockAuthorizationTask);
-    when(mockClearTokenTask.addOnSuccessListener(any())).thenReturn(mockClearTokenTask);
-    when(mockClearTokenTask.addOnFailureListener(any())).thenReturn(mockClearTokenTask);
+    when(mockVoidTask.addOnSuccessListener(any())).thenReturn(mockVoidTask);
+    when(mockVoidTask.addOnFailureListener(any())).thenReturn(mockVoidTask);
     when(mockAuthorizationIntent.getIntentSender()).thenReturn(mockAuthorizationIntentSender);
     when(mockActivityPluginBinding.getActivity()).thenReturn(mockActivity);
 
@@ -1150,9 +1151,39 @@
   }
 
   @Test
+  public void revokeAccess_callsClient() {
+    final List<String> scopes = new ArrayList<>(List.of("openid"));
+    final String accountEmail = "someone@example.com";
+    PlatformRevokeAccessRequest params = new PlatformRevokeAccessRequest(accountEmail, scopes);
+    when(mockAuthorizationClient.revokeAccess(any())).thenReturn(mockVoidTask);
+    plugin.revokeAccess(
+        params,
+        ResultCompat.asCompatCallback(
+            reply -> {
+              return null;
+            }));
+
+    ArgumentCaptor<RevokeAccessRequest> requestCaptor =
+        ArgumentCaptor.forClass(RevokeAccessRequest.class);
+    verify(mockAuthorizationClient).revokeAccess(requestCaptor.capture());
+
+    @SuppressWarnings("unchecked")
+    ArgumentCaptor<OnSuccessListener<Void>> callbackCaptor =
+        ArgumentCaptor.forClass(OnSuccessListener.class);
+    verify(mockVoidTask).addOnSuccessListener(callbackCaptor.capture());
+    callbackCaptor.getValue().onSuccess(null);
+
+    RevokeAccessRequest request = requestCaptor.getValue();
+    assertEquals(scopes.size(), request.getScopes().size());
+    assertEquals(scopes.get(0), request.getScopes().get(0).getScopeUri());
+    // Account is mostly opaque, so just verify that one was set.
+    assertNotNull(request.getAccount());
+  }
+
+  @Test
   public void clearAuthorizationToken_callsClient() {
     final String testToken = "testToken";
-    when(mockAuthorizationClient.clearToken(any())).thenReturn(mockClearTokenTask);
+    when(mockAuthorizationClient.clearToken(any())).thenReturn(mockVoidTask);
     plugin.clearAuthorizationToken(
         testToken,
         ResultCompat.asCompatCallback(
@@ -1167,7 +1198,7 @@
     @SuppressWarnings("unchecked")
     ArgumentCaptor<OnSuccessListener<Void>> callbackCaptor =
         ArgumentCaptor.forClass(OnSuccessListener.class);
-    verify(mockClearTokenTask).addOnSuccessListener(callbackCaptor.capture());
+    verify(mockVoidTask).addOnSuccessListener(callbackCaptor.capture());
     callbackCaptor.getValue().onSuccess(null);
 
     ClearTokenRequest request = authRequestCaptor.getValue();
diff --git a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart
index 81915a6..4cfacbe 100644
--- a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart
+++ b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart
@@ -21,6 +21,9 @@
   String? _serverClientId;
   String? _hostedDomain;
   String? _nonce;
+  // A cache of accounts that have been successfully authenticated via this
+  // plugin instance, and one of the scopes that has been authorized for it.
+  final Map<String, String> _cachedAccounts = <String, String>{};
 
   /// Registers this class as the default instance of [GoogleSignInPlatform].
   static void registerWith() {
@@ -114,10 +117,26 @@
 
   @override
   Future<void> disconnect(DisconnectParams params) async {
-    // TODO(stuartmorgan): Implement this once Credential Manager adds the
-    //  necessary API (or temporarily implement it with the deprecated SDK if
-    //  it becomes a significant issue before the API is added).
-    //  https://github.com/flutter/flutter/issues/169612
+    // AuthorizationClient requires an account, and at least one currently
+    // granted scope, to request revocation. The app-facing API currently
+    // does not take any parameters, and is documented to revoke all authorized
+    // accounts, so disconnect every account that has been authorized.
+    // TODO(stuartmorgan): Consider deprecating the account-less API at the
+    //  app-facing level, and have it instead be an account-level method, to
+    //  better align with the current SDKs.
+    for (final MapEntry<String, String> entry in _cachedAccounts.entries) {
+      // Because revokeAccess removes all authorizations for the app, not just
+      // the scopes provided, (per
+      // https://developer.android.com/identity/authorization#revoke-permissions)
+      // an arbitrary granted scope is used here.
+      await _hostApi.revokeAccess(
+        PlatformRevokeAccessRequest(
+          accountEmail: entry.key,
+          scopes: <String>[entry.value],
+        ),
+      );
+    }
+    _cachedAccounts.clear();
     await signOut(const SignOutParams());
   }
 
@@ -215,6 +234,10 @@
           details: authnResult.details,
         );
       case GetCredentialSuccess():
+        // Store a preliminary entry using the 'openid' scope, which in practice
+        // always seems to be granted at authentication time, so that an account
+        // that is authenticated but never authorized can still be disconnected.
+        _cachedAccounts[authnResult.credential.id] = 'openid';
         return authnResult.credential;
     }
   }
@@ -223,10 +246,11 @@
     AuthorizationRequestDetails request, {
     required bool requestOfflineAccess,
   }) async {
+    final String? email = request.email;
     final AuthorizeResult result = await _hostApi.authorize(
       PlatformAuthorizationRequest(
         scopes: request.scopes,
-        accountEmail: request.email,
+        accountEmail: email,
         hostedDomain: _hostedDomain,
         serverClientIdForForcedRefreshToken:
             requestOfflineAccess ? _serverClientId : null,
@@ -263,6 +287,19 @@
         if (accessToken == null) {
           return (accessToken: null, serverAuthCode: null);
         }
+        // Update the account entry with a scope that was reported as granted,
+        // just in case for some reason 'openid' isn't valid. If the request
+        // wasn't associated with an account, then it won't be available to
+        // disconnect.
+        // TODO(stuartmorgan): If this becomes an issue, see if there is an
+        //  indirect way to get the associated email address that's not
+        //  deprecated.
+        if (email != null) {
+          final String? scope = result.grantedScopes.firstOrNull;
+          if (scope != null) {
+            _cachedAccounts[email] = scope;
+          }
+        }
         return (
           accessToken: accessToken,
           serverAuthCode: result.serverAuthCode,
diff --git a/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart b/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart
index d23bbe6..10bdccd 100644
--- a/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart
+++ b/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart
@@ -271,6 +271,59 @@
   int get hashCode => Object.hashAll(_toList());
 }
 
+/// Parameters for revoking authorization.
+///
+/// Corresponds to the native RevokeAccessRequest.
+/// https://developers.google.com/android/reference/com/google/android/gms/auth/api/identity/RevokeAccessRequest
+class PlatformRevokeAccessRequest {
+  PlatformRevokeAccessRequest({
+    required this.accountEmail,
+    required this.scopes,
+  });
+
+  /// The email for the Google account to revoke authorizations for.
+  String accountEmail;
+
+  /// A list of requested scopes.
+  ///
+  /// Per docs, all granted scopes will be revoked, not only the ones passed
+  /// here. However, at least one currently-granted scope must be provided.
+  List<String> scopes;
+
+  List<Object?> _toList() {
+    return <Object?>[accountEmail, scopes];
+  }
+
+  Object encode() {
+    return _toList();
+  }
+
+  static PlatformRevokeAccessRequest decode(Object result) {
+    result as List<Object?>;
+    return PlatformRevokeAccessRequest(
+      accountEmail: result[0]! as String,
+      scopes: (result[1] as List<Object?>?)!.cast<String>(),
+    );
+  }
+
+  @override
+  // ignore: avoid_equals_and_hash_code_on_mutable_classes
+  bool operator ==(Object other) {
+    if (other is! PlatformRevokeAccessRequest ||
+        other.runtimeType != runtimeType) {
+      return false;
+    }
+    if (identical(this, other)) {
+      return true;
+    }
+    return _deepEquals(encode(), other.encode());
+  }
+
+  @override
+  // ignore: avoid_equals_and_hash_code_on_mutable_classes
+  int get hashCode => Object.hashAll(_toList());
+}
+
 /// Pigeon equivalent of the native GoogleIdTokenCredential.
 class PlatformGoogleIdTokenCredential {
   PlatformGoogleIdTokenCredential({
@@ -555,21 +608,24 @@
     } else if (value is GetCredentialRequestGoogleIdOptionParams) {
       buffer.putUint8(133);
       writeValue(buffer, value.encode());
-    } else if (value is PlatformGoogleIdTokenCredential) {
+    } else if (value is PlatformRevokeAccessRequest) {
       buffer.putUint8(134);
       writeValue(buffer, value.encode());
-    } else if (value is GetCredentialFailure) {
+    } else if (value is PlatformGoogleIdTokenCredential) {
       buffer.putUint8(135);
       writeValue(buffer, value.encode());
-    } else if (value is GetCredentialSuccess) {
+    } else if (value is GetCredentialFailure) {
       buffer.putUint8(136);
       writeValue(buffer, value.encode());
-    } else if (value is AuthorizeFailure) {
+    } else if (value is GetCredentialSuccess) {
       buffer.putUint8(137);
       writeValue(buffer, value.encode());
-    } else if (value is PlatformAuthorizationResult) {
+    } else if (value is AuthorizeFailure) {
       buffer.putUint8(138);
       writeValue(buffer, value.encode());
+    } else if (value is PlatformAuthorizationResult) {
+      buffer.putUint8(139);
+      writeValue(buffer, value.encode());
     } else {
       super.writeValue(buffer, value);
     }
@@ -593,14 +649,16 @@
           readValue(buffer)!,
         );
       case 134:
-        return PlatformGoogleIdTokenCredential.decode(readValue(buffer)!);
+        return PlatformRevokeAccessRequest.decode(readValue(buffer)!);
       case 135:
-        return GetCredentialFailure.decode(readValue(buffer)!);
+        return PlatformGoogleIdTokenCredential.decode(readValue(buffer)!);
       case 136:
-        return GetCredentialSuccess.decode(readValue(buffer)!);
+        return GetCredentialFailure.decode(readValue(buffer)!);
       case 137:
-        return AuthorizeFailure.decode(readValue(buffer)!);
+        return GetCredentialSuccess.decode(readValue(buffer)!);
       case 138:
+        return AuthorizeFailure.decode(readValue(buffer)!);
+      case 139:
         return PlatformAuthorizationResult.decode(readValue(buffer)!);
       default:
         return super.readValueOfType(type, buffer);
@@ -776,4 +834,31 @@
       return (pigeonVar_replyList[0] as AuthorizeResult?)!;
     }
   }
+
+  Future<void> revokeAccess(PlatformRevokeAccessRequest params) async {
+    final String pigeonVar_channelName =
+        'dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.revokeAccess$pigeonVar_messageChannelSuffix';
+    final BasicMessageChannel<Object?> pigeonVar_channel =
+        BasicMessageChannel<Object?>(
+          pigeonVar_channelName,
+          pigeonChannelCodec,
+          binaryMessenger: pigeonVar_binaryMessenger,
+        );
+    final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
+      <Object?>[params],
+    );
+    final List<Object?>? pigeonVar_replyList =
+        await pigeonVar_sendFuture as List<Object?>?;
+    if (pigeonVar_replyList == null) {
+      throw _createConnectionError(pigeonVar_channelName);
+    } else if (pigeonVar_replyList.length > 1) {
+      throw PlatformException(
+        code: pigeonVar_replyList[0]! as String,
+        message: pigeonVar_replyList[1] as String?,
+        details: pigeonVar_replyList[2],
+      );
+    } else {
+      return;
+    }
+  }
 }
diff --git a/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart b/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart
index 4b0f5b8..625ae6d 100644
--- a/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart
+++ b/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart
@@ -66,6 +66,26 @@
   bool autoSelectEnabled;
 }
 
+/// Parameters for revoking authorization.
+///
+/// Corresponds to the native RevokeAccessRequest.
+/// https://developers.google.com/android/reference/com/google/android/gms/auth/api/identity/RevokeAccessRequest
+class PlatformRevokeAccessRequest {
+  PlatformRevokeAccessRequest({
+    required this.accountEmail,
+    required this.scopes,
+  });
+
+  /// The email for the Google account to revoke authorizations for.
+  String accountEmail;
+
+  /// A list of requested scopes.
+  ///
+  /// Per docs, all granted scopes will be revoked, not only the ones passed
+  /// here. However, at least one currently-granted scope must be provided.
+  List<String> scopes;
+}
+
 /// Pigeon equivalent of the native GoogleIdTokenCredential.
 class PlatformGoogleIdTokenCredential {
   String? displayName;
@@ -206,4 +226,7 @@
     PlatformAuthorizationRequest params, {
     required bool promptIfUnauthorized,
   });
+
+  @async
+  void revokeAccess(PlatformRevokeAccessRequest params);
 }
diff --git a/packages/google_sign_in/google_sign_in_android/pubspec.yaml b/packages/google_sign_in/google_sign_in_android/pubspec.yaml
index b1f2292..c45ec03 100644
--- a/packages/google_sign_in/google_sign_in_android/pubspec.yaml
+++ b/packages/google_sign_in/google_sign_in_android/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Android implementation of the google_sign_in plugin.
 repository: https://github.com/flutter/packages/tree/main/packages/google_sign_in/google_sign_in_android
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22
-version: 7.1.0
+version: 7.2.0
 
 environment:
   sdk: ^3.7.0
diff --git a/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart
index 50ea9b4..894999f 100644
--- a/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart
+++ b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart
@@ -959,10 +959,109 @@
     verify(mockApi.clearCredentialState());
   });
 
-  test('disconnect also signs out', () async {
-    await googleSignIn.disconnect(const DisconnectParams());
+  group('disconnect', () {
+    test('calls through with previously authorized accounts', () async {
+      // Populate the cache of users.
+      const String userEmail = 'user@example.com';
+      const String aScope = 'grantedScope';
+      when(mockApi.authorize(any, promptIfUnauthorized: false)).thenAnswer(
+        (_) async => PlatformAuthorizationResult(
+          grantedScopes: <String>[aScope],
+          accessToken: 'token',
+        ),
+      );
+      await googleSignIn.init(const InitParameters(serverClientId: 'id'));
+      await googleSignIn.clientAuthorizationTokensForScopes(
+        const ClientAuthorizationTokensForScopesParameters(
+          request: AuthorizationRequestDetails(
+            scopes: <String>[aScope],
+            userId: null,
+            email: userEmail,
+            promptIfUnauthorized: false,
+          ),
+        ),
+      );
 
-    verify(mockApi.clearCredentialState());
+      await googleSignIn.disconnect(const DisconnectParams());
+
+      final VerificationResult verification = verify(
+        mockApi.revokeAccess(captureAny),
+      );
+      final PlatformRevokeAccessRequest hostParams =
+          verification.captured[0] as PlatformRevokeAccessRequest;
+      expect(hostParams.accountEmail, userEmail);
+      expect(hostParams.scopes.first, aScope);
+    });
+
+    test(
+      'calls through with non-authorized accounts, using "openid"',
+      () async {
+        // Populate the cache of users.
+        when(mockApi.getCredential(any)).thenAnswer(
+          (_) async => GetCredentialSuccess(
+            credential: PlatformGoogleIdTokenCredential(
+              displayName: _testUser.displayName,
+              profilePictureUri: _testUser.photoUrl,
+              id: _testUser.email,
+              idToken: _testAuthnToken.idToken!,
+            ),
+          ),
+        );
+        await googleSignIn.init(const InitParameters(serverClientId: 'id'));
+        await googleSignIn.authenticate(const AuthenticateParameters());
+
+        await googleSignIn.disconnect(const DisconnectParams());
+
+        final VerificationResult verification = verify(
+          mockApi.revokeAccess(captureAny),
+        );
+        final PlatformRevokeAccessRequest hostParams =
+            verification.captured[0] as PlatformRevokeAccessRequest;
+        expect(hostParams.accountEmail, _testUser.email);
+        expect(hostParams.scopes.first, 'openid');
+      },
+    );
+
+    test('does not re-revoke for repeated disconnect', () async {
+      // Populate the cache of users.
+      const String userEmail = 'user@example.com';
+      const String aScope = 'grantedScope';
+      when(mockApi.authorize(any, promptIfUnauthorized: false)).thenAnswer(
+        (_) async => PlatformAuthorizationResult(
+          grantedScopes: <String>[aScope],
+          accessToken: 'token',
+        ),
+      );
+      await googleSignIn.init(const InitParameters(serverClientId: 'id'));
+      await googleSignIn.clientAuthorizationTokensForScopes(
+        const ClientAuthorizationTokensForScopesParameters(
+          request: AuthorizationRequestDetails(
+            scopes: <String>[aScope],
+            userId: null,
+            email: userEmail,
+            promptIfUnauthorized: false,
+          ),
+        ),
+      );
+
+      await googleSignIn.disconnect(const DisconnectParams());
+
+      verify(mockApi.revokeAccess(any));
+
+      reset(mockApi);
+
+      // Since no accounts have authorized since the last disconnect, this
+      // should not attempt to revoke anything.
+      await googleSignIn.disconnect(const DisconnectParams());
+
+      verifyNever(mockApi.revokeAccess(any));
+    });
+
+    test('also signs out', () async {
+      await googleSignIn.disconnect(const DisconnectParams());
+
+      verify(mockApi.clearCredentialState());
+    });
   });
 
   // Returning null triggers the app-facing package to create stream events,
diff --git a/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.mocks.dart b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.mocks.dart
index 5acdee9..45eeb22 100644
--- a/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.mocks.dart
+++ b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.mocks.dart
@@ -124,4 +124,13 @@
             ),
           )
           as _i4.Future<_i2.AuthorizeResult>);
+
+  @override
+  _i4.Future<void> revokeAccess(_i2.PlatformRevokeAccessRequest? params) =>
+      (super.noSuchMethod(
+            Invocation.method(#revokeAccess, [params]),
+            returnValue: _i4.Future<void>.value(),
+            returnValueForMissingStub: _i4.Future<void>.value(),
+          )
+          as _i4.Future<void>);
 }