Skip to content

Commit 8b42e59

Browse files
authored
Google log in (#1000)
* Google log in * Adjustments for Java caller * Fix docs * Add more auth data and sign out once complete
1 parent 3d439ff commit 8b42e59

File tree

8 files changed

+371
-1
lines changed

8 files changed

+371
-1
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ We want to make contributing to this project as easy and transparent as possible
8080
- [Parse Coroutines](/coroutines)
8181
- [ParseUI](https://github.com/parse-community/ParseUI-Android)
8282
- [ParseLiveQuery](https://github.com/parse-community/ParseLiveQuery-Android)
83+
- [ParseGoogleUtils](/google)
8384
- [ParseFacebookUtils](https://github.com/parse-community/ParseFacebookUtils-Android)
8485
- [ParseTwitterUtils](https://github.com/parse-community/ParseTwitterUtils-Android)
8586

google/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

google/README.md

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Parse Google Utils for Android
2+
A utility library to authenticate `ParseUser`s with the Google SDK.
3+
4+
## Setup
5+
6+
## Dependency
7+
8+
After including JitPack:
9+
```gradle
10+
dependencies {
11+
implementation "com.github.parse-community.Parse-SDK-Android:google:latest.version.here"
12+
}
13+
```
14+
15+
## Usage
16+
Here we will show the basic steps for logging in/signing up with Google. First:
17+
```java
18+
// in Application.onCreate(); or somewhere similar
19+
ParseGoogleUtils.initialize(getString(R.string.default_web_client_id));
20+
```
21+
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.
22+
23+
Within the activity where your user is going to log in with Google, include the following:
24+
```java
25+
@Override
26+
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
27+
super.onActivityResult(requestCode, resultCode, data);
28+
ParseGoogleUtils.onActivityResult(requestCode, resultCode, data);
29+
}
30+
```
31+
Then elsewhere, when your user taps the login button:
32+
```java
33+
ParseGoogleUtils.logInWithReadPermissionsInBackground(this, permissions, new LogInCallback() {
34+
@Override
35+
public void done(ParseUser user, ParseException err) {
36+
if (user == null) {
37+
Log.d("MyApp", "Uh oh. The user cancelled the Google login.");
38+
} else if (user.isNew()) {
39+
Log.d("MyApp", "User signed up and logged in through Google!");
40+
} else {
41+
Log.d("MyApp", "User logged in through Google!");
42+
}
43+
}
44+
});
45+
```
46+
47+
## How Do I Contribute?
48+
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).
49+
50+
## License
51+
Copyright (c) 2015-present, Parse, LLC.
52+
All rights reserved.
53+
54+
This source code is licensed under the BSD-style license found in the
55+
LICENSE file in the root directory of this source tree. An additional grant
56+
of patent rights can be found in the PATENTS file in the same directory.

google/build.gradle

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
apply plugin: 'com.android.library'
2+
apply plugin: 'kotlin-android'
3+
4+
android {
5+
compileSdkVersion rootProject.ext.compileSdkVersion
6+
7+
defaultConfig {
8+
minSdkVersion rootProject.ext.minSdkVersion
9+
targetSdkVersion rootProject.ext.targetSdkVersion
10+
versionCode 1
11+
versionName "1.0"
12+
13+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
14+
}
15+
16+
packagingOptions {
17+
exclude "**/BuildConfig.class"
18+
}
19+
20+
lintOptions {
21+
abortOnError false
22+
}
23+
24+
buildTypes {
25+
release {
26+
minifyEnabled false
27+
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
28+
}
29+
}
30+
31+
}
32+
33+
dependencies {
34+
api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
35+
api "com.google.android.gms:play-services-auth:17.0.0"
36+
implementation project(":parse")
37+
}
38+
39+
apply from: "https://raw.githubusercontent.com/Commit451/gradle-android-javadocs/1.1.0/gradle-android-javadocs.gradle"

google/proguard-rules.pro

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Add project specific ProGuard rules here.
2+
# You can control the set of applied configuration files using the
3+
# proguardFiles setting in build.gradle.
4+
#
5+
# For more details, see
6+
# http://developer.android.com/guide/developing/tools/proguard.html
7+
8+
# If your project uses WebView with JS, uncomment the following
9+
# and specify the fully qualified class name to the JavaScript interface
10+
# class:
11+
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12+
# public *;
13+
#}
14+
15+
# Uncomment this to preserve the line number information for
16+
# debugging stack traces.
17+
#-keepattributes SourceFile,LineNumberTable
18+
19+
# If you keep the line number information, uncomment this to
20+
# hide the original source file name.
21+
#-renamesourcefileattribute SourceFile

google/src/main/AndroidManifest.xml

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<manifest package="com.parse.google" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
package com.parse.google
2+
3+
import android.app.Activity
4+
import android.content.Context
5+
import android.content.Intent
6+
import bolts.Continuation
7+
import bolts.Task
8+
import com.google.android.gms.auth.api.signin.GoogleSignIn
9+
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
10+
import com.google.android.gms.auth.api.signin.GoogleSignInClient
11+
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
12+
import com.parse.LogInCallback
13+
import com.parse.ParseException
14+
import com.parse.ParseUser
15+
import com.parse.SaveCallback
16+
17+
/**
18+
* Provides a set of utilities for using Parse with Google.
19+
*/
20+
@Suppress("MemberVisibilityCanBePrivate", "unused")
21+
object ParseGoogleUtils {
22+
23+
private const val AUTH_TYPE = "google"
24+
private var clientId: String? = null
25+
26+
private val lock = Any()
27+
28+
private var isInitialized = false
29+
private var googleSignInClient: GoogleSignInClient? = null
30+
31+
/**
32+
* Just hope this doesn't clash I guess...
33+
*/
34+
private const val REQUEST_CODE_GOOGLE_SIGN_IN = 62987
35+
36+
private var currentCallback: LogInCallback? = null
37+
38+
/**
39+
* Initializes [ParseGoogleUtils] with the [clientId]. If you have Firebase configured, you can
40+
* easily get the [clientId] value via context.getString(R.string.default_web_client_id)
41+
* @param clientId the server clientId
42+
*/
43+
@JvmStatic
44+
fun initialize(clientId: String) {
45+
isInitialized = true
46+
this.clientId = clientId
47+
}
48+
49+
/**
50+
* @param user A [com.parse.ParseUser] object.
51+
* @return `true` if the user is linked to a Facebook account.
52+
*/
53+
@JvmStatic
54+
fun isLinked(user: ParseUser): Boolean {
55+
return user.isLinked(AUTH_TYPE)
56+
}
57+
58+
/**
59+
* Log in using a Google.
60+
*
61+
* @param activity The activity which passes along the result via [onActivityResult]
62+
* @param callback The [LogInCallback] which is invoked on log in success or error
63+
*/
64+
@JvmStatic
65+
fun logIn(activity: Activity, callback: LogInCallback) {
66+
checkInitialization()
67+
this.currentCallback = callback
68+
val googleSignInClient = buildGoogleSignInClient(activity)
69+
this.googleSignInClient = googleSignInClient
70+
activity.startActivityForResult(googleSignInClient.signInIntent, REQUEST_CODE_GOOGLE_SIGN_IN)
71+
}
72+
73+
/**
74+
* The method that should be called from the Activity's or Fragment's onActivityResult method.
75+
*
76+
* @param requestCode The request code that's received by the Activity or Fragment.
77+
* @param resultCode The result code that's received by the Activity or Fragment.
78+
* @param data The result data that's received by the Activity or Fragment.
79+
* @return true if the result could be handled.
80+
*/
81+
@JvmStatic
82+
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
83+
if (requestCode != REQUEST_CODE_GOOGLE_SIGN_IN) {
84+
return false
85+
}
86+
if (requestCode == REQUEST_CODE_GOOGLE_SIGN_IN) {
87+
if (data != null) {
88+
handleSignInResult(data)
89+
} else {
90+
onSignInCallbackResult(null, null)
91+
}
92+
}
93+
return true
94+
}
95+
96+
/**
97+
* Unlink a user from a Facebook account. This will save the user's data.
98+
*
99+
* @param user The user to unlink.
100+
* @param callback A callback that will be executed when unlinking is complete.
101+
* @return A task that will be resolved when linking is complete.
102+
*/
103+
@JvmStatic
104+
fun unlinkInBackground(user: ParseUser, callback: SaveCallback): Task<Void> {
105+
return callbackOnMainThreadAsync(unlinkInBackground(user), callback, false)
106+
}
107+
108+
/**
109+
* Unlink a user from a Google account. This will save the user's data.
110+
*
111+
* @param user The user to unlink.
112+
* @return A task that will be resolved when unlinking has completed.
113+
*/
114+
@JvmStatic
115+
fun unlinkInBackground(user: ParseUser): Task<Void> {
116+
checkInitialization()
117+
return user.unlinkFromInBackground(AUTH_TYPE)
118+
}
119+
120+
/**
121+
* Link an existing Parse user with a Google account using authorization credentials that have
122+
* already been obtained.
123+
*
124+
* @param user The Parse user to link with.
125+
* @param account Authorization credentials of a Google user.
126+
* @return A task that will be resolved when linking is complete.
127+
*/
128+
@JvmStatic
129+
fun linkInBackground(user: ParseUser, account: GoogleSignInAccount): Task<Void> {
130+
return user.linkWithInBackground(AUTH_TYPE, getAuthData(account))
131+
}
132+
133+
private fun checkInitialization() {
134+
synchronized(lock) {
135+
check(isInitialized) { "You must call ParseGoogleUtils.initialize() before using ParseGoogleUtils" }
136+
}
137+
}
138+
139+
private fun handleSignInResult(result: Intent) {
140+
GoogleSignIn.getSignedInAccountFromIntent(result)
141+
.addOnSuccessListener { googleAccount ->
142+
onSignedIn(googleAccount)
143+
}
144+
.addOnFailureListener { exception ->
145+
onSignInCallbackResult(null, exception)
146+
}
147+
}
148+
149+
private fun onSignedIn(account: GoogleSignInAccount) {
150+
googleSignInClient?.signOut()?.addOnCompleteListener {}
151+
val authData: Map<String, String> = getAuthData(account)
152+
ParseUser.logInWithInBackground(AUTH_TYPE, authData)
153+
.continueWith { task ->
154+
when {
155+
task.isCompleted -> {
156+
val user = task.result
157+
onSignInCallbackResult(user, null)
158+
}
159+
task.isFaulted -> {
160+
onSignInCallbackResult(null, task.error)
161+
}
162+
else -> {
163+
onSignInCallbackResult(null, null)
164+
}
165+
}
166+
}
167+
}
168+
169+
private fun getAuthData(account: GoogleSignInAccount): Map<String, String> {
170+
val authData = mutableMapOf<String, String>()
171+
authData["id"] = account.id!!
172+
authData["id_token"] = account.idToken!!
173+
account.email?.let { authData["email"] = it }
174+
account.photoUrl?.let { authData["photo_url"] = it.toString() }
175+
return authData
176+
}
177+
178+
private fun onSignInCallbackResult(user: ParseUser?, exception: Exception?) {
179+
val exceptionToThrow = when (exception) {
180+
is ParseException -> exception
181+
null -> null
182+
else -> ParseException(exception)
183+
}
184+
currentCallback?.done(user, exceptionToThrow)
185+
}
186+
187+
private fun buildGoogleSignInClient(context: Context): GoogleSignInClient {
188+
val signInOptions = GoogleSignInOptions.Builder()
189+
.requestId()
190+
.requestEmail()
191+
.requestIdToken(clientId)
192+
.build()
193+
return GoogleSignIn.getClient(context, signInOptions)
194+
}
195+
196+
/**
197+
* Calls the callback after a task completes on the main thread, returning a Task that completes
198+
* with the same result as the input task after the callback has been run.
199+
*/
200+
private fun <T> callbackOnMainThreadAsync(
201+
task: Task<T>, callback: SaveCallback, reportCancellation: Boolean): Task<T> {
202+
return callbackOnMainThreadInternalAsync(task, callback, reportCancellation)
203+
}
204+
205+
/**
206+
* Calls the callback after a task completes on the main thread, returning a Task that completes
207+
* with the same result as the input task after the callback has been run. If reportCancellation
208+
* is false, the callback will not be called if the task was cancelled.
209+
*/
210+
private fun <T> callbackOnMainThreadInternalAsync(
211+
task: Task<T>, callback: Any?, reportCancellation: Boolean): Task<T> {
212+
if (callback == null) {
213+
return task
214+
}
215+
val tcs: Task<T>.TaskCompletionSource = Task.create()
216+
task.continueWith<Void>(Continuation { task ->
217+
if (task.isCancelled && !reportCancellation) {
218+
tcs.setCancelled()
219+
return@Continuation null
220+
}
221+
Task.UI_THREAD_EXECUTOR.execute {
222+
try {
223+
var error = task.error
224+
if (error != null && error !is ParseException) {
225+
error = ParseException(error)
226+
}
227+
if (callback is SaveCallback) {
228+
callback.done(error as ParseException)
229+
} else if (callback is LogInCallback) {
230+
callback.done(
231+
task.result as ParseUser, error as ParseException)
232+
}
233+
} finally {
234+
when {
235+
task.isCancelled -> {
236+
tcs.setCancelled()
237+
}
238+
task.isFaulted -> {
239+
tcs.setError(task.error)
240+
}
241+
else -> {
242+
tcs.setResult(task.result)
243+
}
244+
}
245+
}
246+
}
247+
null
248+
})
249+
return tcs.task
250+
}
251+
}

settings.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
include ':parse', ':fcm', ':gcm', ':ktx', ':coroutines'
1+
include ':parse', ':fcm', ':gcm', ':ktx', ':coroutines', ':google'
22

0 commit comments

Comments
 (0)