diff --git a/README.md b/README.md
index f03b49075..87baa2704 100644
--- a/README.md
+++ b/README.md
@@ -80,6 +80,7 @@ We want to make contributing to this project as easy and transparent as possible
- [Parse Coroutines](/coroutines)
- [ParseUI](https://github.com/parse-community/ParseUI-Android)
- [ParseLiveQuery](https://github.com/parse-community/ParseLiveQuery-Android)
+ - [ParseGoogleUtils](/google)
- [ParseFacebookUtils](https://github.com/parse-community/ParseFacebookUtils-Android)
- [ParseTwitterUtils](https://github.com/parse-community/ParseTwitterUtils-Android)
diff --git a/google/.gitignore b/google/.gitignore
new file mode 100644
index 000000000..796b96d1c
--- /dev/null
+++ b/google/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/google/README.md b/google/README.md
new file mode 100644
index 000000000..640d2b74a
--- /dev/null
+++ b/google/README.md
@@ -0,0 +1,56 @@
+# Parse Google Utils for Android
+A utility library to authenticate `ParseUser`s with the Google SDK.
+
+## Setup
+
+## Dependency
+
+After including JitPack:
+```gradle
+dependencies {
+ implementation "com.github.parse-community.Parse-SDK-Android:google:latest.version.here"
+}
+```
+
+## Usage
+Here we will show the basic steps for logging in/signing up with Google. First:
+```java
+// in Application.onCreate(); or somewhere similar
+ParseGoogleUtils.initialize(getString(R.string.default_web_client_id));
+```
+If you have already configured [Firebase](https://firebase.google.com/docs/android/setup) in your project, the above will work. Otherwise, you might instead need to replace `getString(R.id.default_web_client_id` with a web configured OAuth 2.0 API client ID.
+
+Within the activity where your user is going to log in with Google, include the following:
+```java
+@Override
+protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ ParseGoogleUtils.onActivityResult(requestCode, resultCode, data);
+}
+```
+Then elsewhere, when your user taps the login button:
+```java
+ParseGoogleUtils.logInWithReadPermissionsInBackground(this, permissions, new LogInCallback() {
+ @Override
+ public void done(ParseUser user, ParseException err) {
+ if (user == null) {
+ Log.d("MyApp", "Uh oh. The user cancelled the Google login.");
+ } else if (user.isNew()) {
+ Log.d("MyApp", "User signed up and logged in through Google!");
+ } else {
+ Log.d("MyApp", "User logged in through Google!");
+ }
+ }
+});
+```
+
+## How Do I Contribute?
+We want to make contributing to this project as easy and transparent as possible. Please refer to the [Contribution Guidelines](https://github.com/parse-community/Parse-SDK-Android/blob/master/CONTRIBUTING.md).
+
+## License
+ Copyright (c) 2015-present, Parse, LLC.
+ All rights reserved.
+
+ This source code is licensed under the BSD-style license found in the
+ LICENSE file in the root directory of this source tree. An additional grant
+ of patent rights can be found in the PATENTS file in the same directory.
diff --git a/google/build.gradle b/google/build.gradle
new file mode 100644
index 000000000..cef2bf984
--- /dev/null
+++ b/google/build.gradle
@@ -0,0 +1,39 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ compileSdkVersion rootProject.ext.compileSdkVersion
+
+ defaultConfig {
+ minSdkVersion rootProject.ext.minSdkVersion
+ targetSdkVersion rootProject.ext.targetSdkVersion
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ packagingOptions {
+ exclude "**/BuildConfig.class"
+ }
+
+ lintOptions {
+ abortOnError false
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+
+}
+
+dependencies {
+ api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ api "com.google.android.gms:play-services-auth:17.0.0"
+ implementation project(":parse")
+}
+
+apply from: "https://raw.githubusercontent.com/Commit451/gradle-android-javadocs/1.1.0/gradle-android-javadocs.gradle"
diff --git a/google/proguard-rules.pro b/google/proguard-rules.pro
new file mode 100644
index 000000000..481bb4348
--- /dev/null
+++ b/google/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/google/src/main/AndroidManifest.xml b/google/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..22892be8f
--- /dev/null
+++ b/google/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/google/src/main/java/com/parse/google/ParseGoogleUtils.kt b/google/src/main/java/com/parse/google/ParseGoogleUtils.kt
new file mode 100644
index 000000000..5358ae45e
--- /dev/null
+++ b/google/src/main/java/com/parse/google/ParseGoogleUtils.kt
@@ -0,0 +1,251 @@
+package com.parse.google
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import bolts.Continuation
+import bolts.Task
+import com.google.android.gms.auth.api.signin.GoogleSignIn
+import com.google.android.gms.auth.api.signin.GoogleSignInAccount
+import com.google.android.gms.auth.api.signin.GoogleSignInClient
+import com.google.android.gms.auth.api.signin.GoogleSignInOptions
+import com.parse.LogInCallback
+import com.parse.ParseException
+import com.parse.ParseUser
+import com.parse.SaveCallback
+
+/**
+ * Provides a set of utilities for using Parse with Google.
+ */
+@Suppress("MemberVisibilityCanBePrivate", "unused")
+object ParseGoogleUtils {
+
+ private const val AUTH_TYPE = "google"
+ private var clientId: String? = null
+
+ private val lock = Any()
+
+ private var isInitialized = false
+ private var googleSignInClient: GoogleSignInClient? = null
+
+ /**
+ * Just hope this doesn't clash I guess...
+ */
+ private const val REQUEST_CODE_GOOGLE_SIGN_IN = 62987
+
+ private var currentCallback: LogInCallback? = null
+
+ /**
+ * Initializes [ParseGoogleUtils] with the [clientId]. If you have Firebase configured, you can
+ * easily get the [clientId] value via context.getString(R.string.default_web_client_id)
+ * @param clientId the server clientId
+ */
+ @JvmStatic
+ fun initialize(clientId: String) {
+ isInitialized = true
+ this.clientId = clientId
+ }
+
+ /**
+ * @param user A [com.parse.ParseUser] object.
+ * @return `true` if the user is linked to a Facebook account.
+ */
+ @JvmStatic
+ fun isLinked(user: ParseUser): Boolean {
+ return user.isLinked(AUTH_TYPE)
+ }
+
+ /**
+ * Log in using a Google.
+ *
+ * @param activity The activity which passes along the result via [onActivityResult]
+ * @param callback The [LogInCallback] which is invoked on log in success or error
+ */
+ @JvmStatic
+ fun logIn(activity: Activity, callback: LogInCallback) {
+ checkInitialization()
+ this.currentCallback = callback
+ val googleSignInClient = buildGoogleSignInClient(activity)
+ this.googleSignInClient = googleSignInClient
+ activity.startActivityForResult(googleSignInClient.signInIntent, REQUEST_CODE_GOOGLE_SIGN_IN)
+ }
+
+ /**
+ * The method that should be called from the Activity's or Fragment's onActivityResult method.
+ *
+ * @param requestCode The request code that's received by the Activity or Fragment.
+ * @param resultCode The result code that's received by the Activity or Fragment.
+ * @param data The result data that's received by the Activity or Fragment.
+ * @return true if the result could be handled.
+ */
+ @JvmStatic
+ fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
+ if (requestCode != REQUEST_CODE_GOOGLE_SIGN_IN) {
+ return false
+ }
+ if (requestCode == REQUEST_CODE_GOOGLE_SIGN_IN) {
+ if (data != null) {
+ handleSignInResult(data)
+ } else {
+ onSignInCallbackResult(null, null)
+ }
+ }
+ return true
+ }
+
+ /**
+ * Unlink a user from a Facebook account. This will save the user's data.
+ *
+ * @param user The user to unlink.
+ * @param callback A callback that will be executed when unlinking is complete.
+ * @return A task that will be resolved when linking is complete.
+ */
+ @JvmStatic
+ fun unlinkInBackground(user: ParseUser, callback: SaveCallback): Task {
+ return callbackOnMainThreadAsync(unlinkInBackground(user), callback, false)
+ }
+
+ /**
+ * Unlink a user from a Google account. This will save the user's data.
+ *
+ * @param user The user to unlink.
+ * @return A task that will be resolved when unlinking has completed.
+ */
+ @JvmStatic
+ fun unlinkInBackground(user: ParseUser): Task {
+ checkInitialization()
+ return user.unlinkFromInBackground(AUTH_TYPE)
+ }
+
+ /**
+ * Link an existing Parse user with a Google account using authorization credentials that have
+ * already been obtained.
+ *
+ * @param user The Parse user to link with.
+ * @param account Authorization credentials of a Google user.
+ * @return A task that will be resolved when linking is complete.
+ */
+ @JvmStatic
+ fun linkInBackground(user: ParseUser, account: GoogleSignInAccount): Task {
+ return user.linkWithInBackground(AUTH_TYPE, getAuthData(account))
+ }
+
+ private fun checkInitialization() {
+ synchronized(lock) {
+ check(isInitialized) { "You must call ParseGoogleUtils.initialize() before using ParseGoogleUtils" }
+ }
+ }
+
+ private fun handleSignInResult(result: Intent) {
+ GoogleSignIn.getSignedInAccountFromIntent(result)
+ .addOnSuccessListener { googleAccount ->
+ onSignedIn(googleAccount)
+ }
+ .addOnFailureListener { exception ->
+ onSignInCallbackResult(null, exception)
+ }
+ }
+
+ private fun onSignedIn(account: GoogleSignInAccount) {
+ googleSignInClient?.signOut()?.addOnCompleteListener {}
+ val authData: Map = getAuthData(account)
+ ParseUser.logInWithInBackground(AUTH_TYPE, authData)
+ .continueWith { task ->
+ when {
+ task.isCompleted -> {
+ val user = task.result
+ onSignInCallbackResult(user, null)
+ }
+ task.isFaulted -> {
+ onSignInCallbackResult(null, task.error)
+ }
+ else -> {
+ onSignInCallbackResult(null, null)
+ }
+ }
+ }
+ }
+
+ private fun getAuthData(account: GoogleSignInAccount): Map {
+ val authData = mutableMapOf()
+ authData["id"] = account.id!!
+ authData["id_token"] = account.idToken!!
+ account.email?.let { authData["email"] = it }
+ account.photoUrl?.let { authData["photo_url"] = it.toString() }
+ return authData
+ }
+
+ private fun onSignInCallbackResult(user: ParseUser?, exception: Exception?) {
+ val exceptionToThrow = when (exception) {
+ is ParseException -> exception
+ null -> null
+ else -> ParseException(exception)
+ }
+ currentCallback?.done(user, exceptionToThrow)
+ }
+
+ private fun buildGoogleSignInClient(context: Context): GoogleSignInClient {
+ val signInOptions = GoogleSignInOptions.Builder()
+ .requestId()
+ .requestEmail()
+ .requestIdToken(clientId)
+ .build()
+ return GoogleSignIn.getClient(context, signInOptions)
+ }
+
+ /**
+ * Calls the callback after a task completes on the main thread, returning a Task that completes
+ * with the same result as the input task after the callback has been run.
+ */
+ private fun callbackOnMainThreadAsync(
+ task: Task, callback: SaveCallback, reportCancellation: Boolean): Task {
+ return callbackOnMainThreadInternalAsync(task, callback, reportCancellation)
+ }
+
+ /**
+ * Calls the callback after a task completes on the main thread, returning a Task that completes
+ * with the same result as the input task after the callback has been run. If reportCancellation
+ * is false, the callback will not be called if the task was cancelled.
+ */
+ private fun callbackOnMainThreadInternalAsync(
+ task: Task, callback: Any?, reportCancellation: Boolean): Task {
+ if (callback == null) {
+ return task
+ }
+ val tcs: Task.TaskCompletionSource = Task.create()
+ task.continueWith(Continuation { task ->
+ if (task.isCancelled && !reportCancellation) {
+ tcs.setCancelled()
+ return@Continuation null
+ }
+ Task.UI_THREAD_EXECUTOR.execute {
+ try {
+ var error = task.error
+ if (error != null && error !is ParseException) {
+ error = ParseException(error)
+ }
+ if (callback is SaveCallback) {
+ callback.done(error as ParseException)
+ } else if (callback is LogInCallback) {
+ callback.done(
+ task.result as ParseUser, error as ParseException)
+ }
+ } finally {
+ when {
+ task.isCancelled -> {
+ tcs.setCancelled()
+ }
+ task.isFaulted -> {
+ tcs.setError(task.error)
+ }
+ else -> {
+ tcs.setResult(task.result)
+ }
+ }
+ }
+ }
+ null
+ })
+ return tcs.task
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index afc5327fa..ac88baeb6 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,2 +1,2 @@
-include ':parse', ':fcm', ':gcm', ':ktx', ':coroutines'
+include ':parse', ':fcm', ':gcm', ':ktx', ':coroutines', ':google'