From 20d6d947c603193826695cd248ca35f4449b5309 Mon Sep 17 00:00:00 2001 From: John Date: Mon, 10 Feb 2020 01:35:09 -0600 Subject: [PATCH 1/4] Google log in --- README.md | 1 + google/.gitignore | 1 + google/README.md | 56 ++++ google/build.gradle | 39 +++ google/proguard-rules.pro | 21 ++ google/src/main/AndroidManifest.xml | 1 + .../java/com/parse/google/ParseGoogleUtils.kt | 239 ++++++++++++++++++ settings.gradle | 2 +- 8 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 google/.gitignore create mode 100644 google/README.md create mode 100644 google/build.gradle create mode 100644 google/proguard-rules.pro create mode 100644 google/src/main/AndroidManifest.xml create mode 100644 google/src/main/java/com/parse/google/ParseGoogleUtils.kt 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..30e570547 --- /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 +Extensive docs can be found in the [guide][guide]. The basic steps are: +```java +// in Application.onCreate(); or somewhere similar +ParseGoogleUtils.initialize(context, 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..c57dafb03 --- /dev/null +++ b/google/src/main/java/com/parse/google/ParseGoogleUtils.kt @@ -0,0 +1,239 @@ +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 + + /** + * 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 + */ + 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. + */ + 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 + */ + fun logIn(activity: Activity, callback: LogInCallback) { + checkInitialization() + this.currentCallback = callback + val googleSignInClient = buildGoogleSignInClient(activity) + 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. + */ + 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. + */ + 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. + */ + 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. + */ + 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) { + 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!! + 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' From cbafcb94f6fb78d7df3439b4dfa7010939274085 Mon Sep 17 00:00:00 2001 From: John Date: Mon, 10 Feb 2020 01:39:28 -0600 Subject: [PATCH 2/4] Adjustments for Java caller --- google/README.md | 2 +- google/src/main/java/com/parse/google/ParseGoogleUtils.kt | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/google/README.md b/google/README.md index 30e570547..33e97b311 100644 --- a/google/README.md +++ b/google/README.md @@ -13,7 +13,7 @@ dependencies { ``` ## Usage -Extensive docs can be found in the [guide][guide]. The basic steps are: +Here we will show the basic steps for logging in/signing up with Google. First: ```java // in Application.onCreate(); or somewhere similar ParseGoogleUtils.initialize(context, getString(R.string.default_web_client_id)); diff --git a/google/src/main/java/com/parse/google/ParseGoogleUtils.kt b/google/src/main/java/com/parse/google/ParseGoogleUtils.kt index c57dafb03..d2f5a8c5e 100644 --- a/google/src/main/java/com/parse/google/ParseGoogleUtils.kt +++ b/google/src/main/java/com/parse/google/ParseGoogleUtils.kt @@ -39,6 +39,7 @@ object ParseGoogleUtils { * 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 @@ -48,6 +49,7 @@ object ParseGoogleUtils { * @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) } @@ -58,6 +60,7 @@ object ParseGoogleUtils { * @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 @@ -73,6 +76,7 @@ object ParseGoogleUtils { * @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 @@ -94,6 +98,7 @@ object ParseGoogleUtils { * @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) } @@ -104,6 +109,7 @@ object ParseGoogleUtils { * @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) @@ -117,6 +123,7 @@ object ParseGoogleUtils { * @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)) } From de7a20cb3f7af318a8c84a7455981105cb7c700d Mon Sep 17 00:00:00 2001 From: John Date: Mon, 10 Feb 2020 01:40:51 -0600 Subject: [PATCH 3/4] Fix docs --- google/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google/README.md b/google/README.md index 33e97b311..640d2b74a 100644 --- a/google/README.md +++ b/google/README.md @@ -16,7 +16,7 @@ dependencies { Here we will show the basic steps for logging in/signing up with Google. First: ```java // in Application.onCreate(); or somewhere similar -ParseGoogleUtils.initialize(context, getString(R.string.default_web_client_id)); +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. From 7a33a490e5f27cbb79dc601c64474c4eec8b2ea3 Mon Sep 17 00:00:00 2001 From: John Date: Mon, 10 Feb 2020 02:00:27 -0600 Subject: [PATCH 4/4] Add more auth data and sign out once complete --- google/src/main/java/com/parse/google/ParseGoogleUtils.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/google/src/main/java/com/parse/google/ParseGoogleUtils.kt b/google/src/main/java/com/parse/google/ParseGoogleUtils.kt index d2f5a8c5e..5358ae45e 100644 --- a/google/src/main/java/com/parse/google/ParseGoogleUtils.kt +++ b/google/src/main/java/com/parse/google/ParseGoogleUtils.kt @@ -26,6 +26,7 @@ object ParseGoogleUtils { private val lock = Any() private var isInitialized = false + private var googleSignInClient: GoogleSignInClient? = null /** * Just hope this doesn't clash I guess... @@ -65,6 +66,7 @@ object ParseGoogleUtils { checkInitialization() this.currentCallback = callback val googleSignInClient = buildGoogleSignInClient(activity) + this.googleSignInClient = googleSignInClient activity.startActivityForResult(googleSignInClient.signInIntent, REQUEST_CODE_GOOGLE_SIGN_IN) } @@ -145,6 +147,7 @@ object ParseGoogleUtils { } private fun onSignedIn(account: GoogleSignInAccount) { + googleSignInClient?.signOut()?.addOnCompleteListener {} val authData: Map = getAuthData(account) ParseUser.logInWithInBackground(AUTH_TYPE, authData) .continueWith { task -> @@ -167,6 +170,8 @@ object ParseGoogleUtils { 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 }