Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
ext.kotlin_version = '1.6.21'
ext.kotlin_version = '1.8.22'
ext.koin_version = '2.1.6'
ext.lifecycle_version = '2.2.0'
ext.lifecycle_version = '2.5.0'
ext.jacoco_core_version = '0.8.7'
repositories {
google()
Expand Down
17 changes: 13 additions & 4 deletions catroid/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.google.gms:google-services:4.3.10'
classpath 'com.google.gms:google-services:4.4.2'
}
}

Expand Down Expand Up @@ -200,7 +200,9 @@ android {
excludes += ['lib/mips/*', 'lib/armeabi/*']
}
resources {
excludes += ['LICENSE.txt', 'META-INF/LICENSE.md', 'META-INF/INDEX.LIST', 'lib/mips/*', 'lib/armeabi/*']
excludes += ['LICENSE.txt', 'META-INF/DEPENDENCIES', 'META-INF/LICENSE.md',
'META-INF/INDEX.LIST',
'lib/mips/*', 'lib/armeabi/*']
}
}

Expand Down Expand Up @@ -381,6 +383,9 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.mediarouter:mediarouter:1.1.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.credentials:credentials:1.3.0'
implementation 'androidx.credentials:credentials-play-services-auth:1.3.0'
implementation 'com.google.android.libraries.identity.googleid:googleid:1.1.1'
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.browser:browser:1.2.0'
implementation 'androidx.core:core-ktx:1.3.2'
Expand Down Expand Up @@ -409,7 +414,6 @@ dependencies {
implementation 'com.github.bumptech.glide:glide:4.11.0'

// Lifecycle
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"

Expand Down Expand Up @@ -439,6 +443,11 @@ dependencies {
exclude group: 'xmlpull'
}

// Client
implementation 'com.google.api-client:google-api-client:2.7.0'
implementation 'com.google.oauth-client:google-oauth-client:1.37.0'
implementation 'com.google.http-client:google-http-client-jackson2:1.42.0'

// Catblocks
implementation "androidx.webkit:webkit:1.2.0"

Expand Down Expand Up @@ -482,7 +491,7 @@ dependencies {
natives "com.badlogicgames.gdx:gdx-box2d-platform:$gdxVersion:natives-armeabi-v7a"
natives "com.badlogicgames.gdx:gdx-box2d-platform:$gdxVersion:natives-arm64-v8a"

implementation "com.google.android.gms:play-services-auth:17.0.0"
implementation "com.google.android.gms:play-services-auth:20.6.0"

androidTestImplementation('tools.fastlane:screengrab:2.1.1') {
// https://issuetracker.google.com/issues/123060356
Expand Down
6 changes: 6 additions & 0 deletions catroid/proguard-project.txt
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,9 @@
public static int d(...);
public static int e(...);
}

# Credentials Manager
-if class androidx.credentials.CredentialManager
-keep class androidx.credentials.playservices.** {
*;
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.util.Log;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* Catroid: An on-device visual programming system for Android devices
* Copyright (C) 2010-2022 The Catrobat Team
* Copyright (C) 2010-2025 The Catrobat Team
* (<http://developer.catrobat.org/credits>)
*
* This program is free software: you can redistribute it and/or modify
Expand All @@ -23,82 +23,132 @@

package org.catrobat.catroid.transfers;

import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.preference.PreferenceManager;
import android.util.Log;

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.google.android.gms.tasks.Task;
import com.google.android.gms.auth.api.identity.AuthorizationClient;
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.Identity;
import com.google.android.gms.common.api.ApiException;
import com.google.android.gms.common.api.Scope;
import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption;
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential;

import org.catrobat.catroid.R;
import org.catrobat.catroid.common.Constants;
import org.catrobat.catroid.ui.BaseActivity;
import org.catrobat.catroid.ui.recyclerview.dialog.login.OAuthUsernameDialogFragment;
import org.catrobat.catroid.ui.recyclerview.dialog.login.SignInCompleteListener;
import org.catrobat.catroid.utils.DeviceSettingsProvider;
import org.catrobat.catroid.utils.ToastUtil;

import androidx.appcompat.app.AppCompatActivity;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Executor;

import static com.google.android.gms.auth.api.signin.GoogleSignIn.getClient;
import static com.google.android.gms.auth.api.signin.GoogleSignIn.getSignedInAccountFromIntent;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.credentials.Credential;
import androidx.credentials.CredentialManager;
import androidx.credentials.CredentialManagerCallback;
import androidx.credentials.GetCredentialRequest;
import androidx.credentials.GetCredentialResponse;
import androidx.credentials.exceptions.GetCredentialException;

import static org.catrobat.catroid.web.ServerAuthenticationConstants.GOOGLE_LOGIN_CATROWEB_SERVER_CLIENT_ID;

public class GoogleLoginHandler implements CheckOAuthTokenTask.OnCheckOAuthTokenCompleteListener,
GoogleLogInTask.OnGoogleServerLogInCompleteListener,
public class GoogleLoginHandler extends BaseActivity implements GoogleLogInTask.OnGoogleServerLogInCompleteListener,
CheckEmailAvailableTask.OnCheckEmailAvailableCompleteListener,
CheckOAuthTokenTask.OnCheckOAuthTokenCompleteListener,
GoogleVerifyUserTask.OnGoogleVerifyUserCompleteListener,
GoogleExchangeCodeTask.OnGoogleExchangeCodeCompleteListener {

public static final int REQUEST_CODE_GOOGLE_AUTHORIZATION = 111;

private AppCompatActivity activity;
public static final int REQUEST_CODE_GOOGLE_SIGNIN = 100;
private GoogleSignInClient googleSignInClient;

@SuppressWarnings("RestrictedApi")
public GoogleLoginHandler(AppCompatActivity activity) {
this.activity = activity;

GoogleSignInOptions googleSignInOptions = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestEmail()
.requestIdToken(GOOGLE_LOGIN_CATROWEB_SERVER_CLIENT_ID)
.build();
googleSignInClient = getClient(this.activity, googleSignInOptions);
}

public GoogleSignInClient getGoogleSignInClient() {
return googleSignInClient;
}
public void signInWithGoogle() {
CredentialManager credentialManager = CredentialManager.create(activity);

@SuppressWarnings("RestrictedApi")
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_GOOGLE_SIGNIN) {
Task<GoogleSignInAccount> task = getSignedInAccountFromIntent(data);
if (task.isSuccessful()) {
onGoogleLogInComplete(task.getResult());
} else {
ToastUtil.showError(activity,
String.format(activity.getString(R.string.error_google_plus_sign_in), task.getException().getLocalizedMessage().replace(":", "")));
StringBuilder hashedNonce = new StringBuilder();
try {
String rawNonce = UUID.randomUUID().toString();
byte[] bytes = rawNonce.getBytes();
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(bytes);

for (byte b : digest) {
hashedNonce.append(String.format("%02x", b));
}
} catch (Exception e) {
Log.i("Google", "Creating nonce has failed.");
e.printStackTrace();
}

GetSignInWithGoogleOption googleSignInOption =
new GetSignInWithGoogleOption.Builder(GOOGLE_LOGIN_CATROWEB_SERVER_CLIENT_ID)
.setNonce(hashedNonce.toString())
.build();
GetCredentialRequest credentialRequest = new GetCredentialRequest.Builder()
.addCredentialOption(googleSignInOption)
.build();
Executor executor = ContextCompat.getMainExecutor(activity);
CancellationSignal cancellationSignal = new CancellationSignal();
GoogleLoginHandler googleLoginHandler = this;
credentialManager.getCredentialAsync(
activity,
credentialRequest,
cancellationSignal,
executor,
new CredentialManagerCallback<GetCredentialResponse, GetCredentialException>() {
@Override
public void onResult(GetCredentialResponse getCredentialResponse) {
Credential credential = getCredentialResponse.getCredential();
GoogleIdTokenCredential tokenCredential =
GoogleIdTokenCredential.createFrom(credential.getData());

GoogleVerifyUserTask googleVerifyUserTask = new GoogleVerifyUserTask(tokenCredential);
googleVerifyUserTask.setOnGoogleVerifyUserCompleteListener(googleLoginHandler);
googleVerifyUserTask.execute();
}

@Override
public void onError(@NonNull GetCredentialException e) {
ToastUtil.showError(activity,
String.format(activity.getString(R.string.error_google_plus_sign_in), e.getLocalizedMessage().replace(":", "")));
e.printStackTrace();
}
}
);
}

public void onGoogleLogInComplete(GoogleSignInAccount account) {
@Override
public void onGoogleVerifyUserComplete(GoogleIdTokenCredential account, String googleEmail) {
String id = account.getId();
String personName = account.getDisplayName();
String email = account.getEmail();
String locale = DeviceSettingsProvider.getUserCountryCode();
String idToken = account.getIdToken();
String code = account.getServerAuthCode();

PreferenceManager.getDefaultSharedPreferences(activity).edit()
.putString(Constants.GOOGLE_ID, id)
.putString(Constants.GOOGLE_USERNAME, personName)
.putString(Constants.GOOGLE_EMAIL, email)
.putString(Constants.GOOGLE_EMAIL, googleEmail)
.putString(Constants.GOOGLE_LOCALE, locale)
.putString(Constants.GOOGLE_ID_TOKEN, idToken)
.putString(Constants.GOOGLE_EXCHANGE_CODE, code)
.apply();

CheckOAuthTokenTask checkOAuthTokenTask = new CheckOAuthTokenTask(activity, id, Constants.GOOGLE_PLUS);
Expand All @@ -125,23 +175,62 @@ public void onCheckOAuthTokenComplete(Boolean tokenAvailable, String provider) {
}
}

@Override
public void onGoogleServerLogInComplete() {
Bundle bundle = new Bundle();
bundle.putString(Constants.CURRENT_OAUTH_PROVIDER, Constants.GOOGLE_PLUS);
((SignInCompleteListener) activity).onLoginSuccessful(bundle);
}

@Override
public void onCheckEmailAvailableComplete(Boolean emailAvailable, String provider) {
if (emailAvailable) {
exchangeGoogleAuthorizationCode();
authorizeGoogleUser();
} else {
showOauthUserNameDialog(Constants.GOOGLE_PLUS);
}
}

public void exchangeGoogleAuthorizationCode() {
public void authorizeGoogleUser() {
AuthorizationRequest authorizationRequest = new AuthorizationRequest.Builder()
.requestOfflineAccess(GOOGLE_LOGIN_CATROWEB_SERVER_CLIENT_ID)
.setRequestedScopes(List.of(
new Scope("https://www.googleapis.com/auth/userinfo.email") // Request email scope
))
.build();

AuthorizationClient authorizationClient = Identity.getAuthorizationClient(activity);
authorizationClient.authorize(authorizationRequest).addOnSuccessListener(authorizationResult -> {
if (authorizationResult.hasResolution()) {
try {
authorizationResult.getPendingIntent().send(REQUEST_CODE_GOOGLE_AUTHORIZATION);
} catch (Exception e) {
Log.e("Google", "Failed to start authorization UI: " + e.getLocalizedMessage());
}
} else {
PreferenceManager.getDefaultSharedPreferences(activity).edit()
.putString(Constants.GOOGLE_EXCHANGE_CODE, authorizationResult.getServerAuthCode())
.apply();
exchangeGoogleServerAuthCode();
}
}).addOnFailureListener(e -> Log.e("Google", "Authorization failed", e));
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);

if (requestCode == REQUEST_CODE_GOOGLE_AUTHORIZATION && resultCode == Activity.RESULT_OK) {
AuthorizationClient authorizationClient = Identity.getAuthorizationClient(this);
AuthorizationResult authorizationResult;

try {
authorizationResult = authorizationClient.getAuthorizationResultFromIntent(data);
} catch (ApiException e) {
throw new RuntimeException(e);
}

PreferenceManager.getDefaultSharedPreferences(activity).edit()
.putString(Constants.GOOGLE_EXCHANGE_CODE, authorizationResult.getServerAuthCode())
.apply();
exchangeGoogleServerAuthCode();
}
}

private void exchangeGoogleServerAuthCode() {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity);
GoogleExchangeCodeTask googleExchangeCodeTask = new GoogleExchangeCodeTask(activity,
sharedPreferences.getString(Constants.GOOGLE_EXCHANGE_CODE, Constants.NO_GOOGLE_EXCHANGE_CODE),
Expand All @@ -154,15 +243,6 @@ public void exchangeGoogleAuthorizationCode() {
googleExchangeCodeTask.execute();
}

private void showOauthUserNameDialog(String provider) {
OAuthUsernameDialogFragment dialog = new OAuthUsernameDialogFragment();
Bundle bundle = new Bundle();
bundle.putString(Constants.CURRENT_OAUTH_PROVIDER, provider);
dialog.setArguments(bundle);
dialog.setSignInCompleteListener((SignInCompleteListener) activity);
dialog.show(activity.getSupportFragmentManager(), OAuthUsernameDialogFragment.TAG);
}

@Override
public void onGoogleExchangeCodeComplete() {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity);
Expand All @@ -174,4 +254,20 @@ public void onGoogleExchangeCodeComplete() {
googleLogInTask.setOnGoogleServerLogInCompleteListener(this);
googleLogInTask.execute();
}

@Override
public void onGoogleServerLogInComplete() {
Bundle bundle = new Bundle();
bundle.putString(Constants.CURRENT_OAUTH_PROVIDER, Constants.GOOGLE_PLUS);
((SignInCompleteListener) activity).onLoginSuccessful(bundle);
}

private void showOauthUserNameDialog(String provider) {
OAuthUsernameDialogFragment dialog = new OAuthUsernameDialogFragment();
Bundle bundle = new Bundle();
bundle.putString(Constants.CURRENT_OAUTH_PROVIDER, provider);
dialog.setArguments(bundle);
dialog.setSignInCompleteListener((SignInCompleteListener) activity);
dialog.show(activity.getSupportFragmentManager(), OAuthUsernameDialogFragment.TAG);
}
}
Loading