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
66 changes: 66 additions & 0 deletions play-services-core-proto/src/main/proto/games.proto
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,72 @@ option java_multiple_files = true;
service PlayersFirstParty {
rpc DeleteApplicationDataFirstParty (DeleteApplicationDataRequest) returns (DeleteApplicationDataResponse);
rpc DeletePlayerFirstParty (DeletePlayerRequest) returns (DeletePlayerResponse);
rpc GetPlayerFirstParty (GetPlayerFirstPartyRequest) returns (GetPlayerFirstPartyResponse);
rpc GetUiSignInDataFirstParty (GetUiSignInDataFirstPartyRequest) returns (GetUiSignInDataFirstPartyResponse);
rpc CheckGamerTagFirstParty (CheckGamerTagFirstPartyRequest) returns (CheckGamerTagFirstPartyResponse);
rpc UpdateProfileSettingsFirstParty (UpdateProfileSettingsFirstPartyRequest) returns (UpdateProfileSettingsFirstPartyResponse);
}

message GetUiSignInDataFirstPartyRequest {}

message GetUiSignInDataFirstPartyResponse {
optional int32 status = 1;
optional UiSignInProfileSettings profile_settings = 2;
optional string token = 3;
}

message UiSignInProfileSettings {
optional string kind = 1;
optional bool settings_changes_prohibited = 2;
optional bool profile_visible = 3;
optional string gamer_tag = 4;
optional bool profile_discoverable_via_google_account = 7;
optional string stock_gamer_avatar_url = 8;
}

message CheckGamerTagFirstPartyRequest {
optional string gamer_tag = 1;
optional string region = 2;
}

message CheckGamerTagFirstPartyResponse {
optional int32 status = 1;
}

message GetPlayerFirstPartyRequest {
optional string locale = 1;
optional string player_id = 2;
}

message GetPlayerFirstPartyResponse {
optional string kind = 1;
optional bytes player_data = 2;
}

message UpdateProfileSettingsFirstPartyRequest {
optional string locale = 2;
optional ProfileSettings profile_settings = 3;
}

message ProfileSettings {
optional bool profile_visible = 3;
optional string gamer_tag = 4;
optional bool profile_visibility_was_chosen_by_player = 5;
optional bool profile_discoverable_via_google_account = 6;
optional bool gamer_tag_is_explicitly_set = 7;
optional string stock_gamer_avatar_url = 8;
optional bool always_auto_sign_in = 11;
optional bool auto_sign_in = 13;
optional bool games_lite_player_stats_enabled = 19;
optional int32 friends_list_visibility = 22;
optional int32 play_together_status = 23;
optional int32 gamer_tag_source = 24;
}

message UpdateProfileSettingsFirstPartyResponse {
optional string kind = 1;
optional int32 status = 2;
optional string player_id = 4;
}

service ApplicationsFirstParty {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ class GamesConnectServiceImpl(val context: Context, override val lifecycle: Life
Log.d(TAG, "signIn success")
callback?.onSignIn(Status.SUCCESS, GamesSignInResponse().apply { gameRunToken = UUID.randomUUID().toString() })
} else {
sendSignInRequired()
runCatching { sendSignInRequired() }.onFailure { Log.w(TAG, "signIn fail", it) }
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
Expand Down Expand Up @@ -115,8 +115,13 @@ class GamesService : BaseService(TAG, GmsService.GAMES) {
return@launchWhenStarted sendSignInRequired(account)
}

if (!performGamesSignIn(this@GamesService, packageName, account, scopes = scopes)) {
Log.d(TAG, "performGamesSignIn fail, sign in required")
try {
if (!performGamesSignIn(this@GamesService, packageName, account, scopes = scopes)) {
Log.d(TAG, "performGamesSignIn fail, sign in required")
return@launchWhenStarted sendSignInRequired(account)
}
} catch (e: Exception) {
Log.w(TAG, "performGamesSignIn exception, sign in required", e)
return@launchWhenStarted sendSignInRequired(account)
}

Expand Down Expand Up @@ -793,4 +798,4 @@ class GamesServiceImpl(val context: Context, override val lifecycle: Lifecycle,

override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean =
warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) }
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
Expand Down Expand Up @@ -78,12 +78,17 @@ class GamesSignInActivity : AppCompatActivity() {
?.getParcelable<GoogleSignInAccount>("googleSignInAccount")?.account
if (account != null) {
lifecycleScope.launchWhenStarted {
signIn(account)
try {
signIn(account)
} catch (e: Exception) {
Log.w(TAG, "signIn failed", e)
finish()
}
}
return
}
}
finish()
}
}
}
}
116 changes: 54 additions & 62 deletions play-services-core/src/main/kotlin/org/microg/gms/games/extensions.kt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
Expand Down Expand Up @@ -33,7 +33,9 @@ import com.google.android.gms.games.PlayerLevelInfo
import com.google.android.gms.games.PlayerRelationshipInfoEntity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import com.squareup.wire.GrpcClient
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import org.json.JSONObject
import org.microg.gms.auth.AuthConstants
import org.microg.gms.auth.AuthManager
Expand All @@ -51,6 +53,7 @@ import org.microg.gms.utils.singleInstanceOf
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.suspendCancellableCoroutine

const val SERVICE_GAMES_LITE = "oauth2:https://www.googleapis.com/auth/games_lite"

Expand Down Expand Up @@ -217,53 +220,34 @@ suspend fun requestGamesInfo(
})
}

suspend fun registerForGames(context: Context, account: Account, queue: RequestQueue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) }) {
val authManager = AuthManager(context, account.name, Constants.GMS_PACKAGE_NAME, "oauth2:${Scopes.GAMES_FIRSTPARTY}")
authManager.setOauth2Foreground("1")
val authToken = withContext(Dispatchers.IO) { authManager.requestAuthWithBackgroundResolution(false).auth }
val androidId = getSettings(context, CheckIn.getContentUri(context), arrayOf(CheckIn.ANDROID_ID)) { cursor: Cursor -> cursor.getLong(0) }
val result = suspendCoroutine<JSONObject> { continuation ->
queue.add(
object : JsonObjectRequest(
"https://www.googleapis.com/games/v1whitelisted/players/me/profilesettings?requestRandomGamerTag=true&language=${Utils.getLocale(context)}",
{ continuation.resume(it) },
{ continuation.resumeWithException(RuntimeException(it)) }) {
override fun getHeaders(): MutableMap<String, String> {
return mutableMapOf(
"Authorization" to "OAuth $authToken",
"X-Device-ID" to androidId.toString(16)
)
}
}
suspend fun registerForGames(context: Context, account: Account) {
withContext(Dispatchers.IO) {
val authManager = AuthManager(context, account.name, Constants.GMS_PACKAGE_NAME, "oauth2:${Scopes.GAMES_FIRSTPARTY}")
authManager.setOauth2Foreground("1")
val authToken = authManager.requestAuthWithBackgroundResolution(false).auth ?: throw RuntimeException("authToken is null")
val client = OkHttpClient().newBuilder().addInterceptor(HeaderInterceptor(context, authToken)).build()
val grpcClient = GrpcClient.Builder().client(client).baseUrl("https://gameswhitelisted.googleapis.com").build()
val playersClient = grpcClient.create(PlayersFirstPartyClient::class)
val locale = Utils.getLocale(context).toString()
val signInData = playersClient.GetUiSignInDataFirstParty().execute(GetUiSignInDataFirstPartyRequest())
val gamerTag = signInData.profile_settings?.gamer_tag ?: throw RuntimeException("No gamerTag returned")
val avatarUrl = signInData.profile_settings?.stock_gamer_avatar_url
val profileSettings = ProfileSettings(
profile_visible = false,
gamer_tag = gamerTag,
profile_visibility_was_chosen_by_player = false,
profile_discoverable_via_google_account = false,
gamer_tag_is_explicitly_set = false,
stock_gamer_avatar_url = avatarUrl,
always_auto_sign_in = false,
auto_sign_in = false,
games_lite_player_stats_enabled = false,
friends_list_visibility = 2,
play_together_status = 2,
gamer_tag_source = 0
)
}
suspendCoroutine<JSONObject> { continuation ->
queue.add(
object : JsonObjectRequest(
Method.PUT,
"https://www.googleapis.com/games/v1whitelisted/players/me/profilesettings?language=${Utils.getLocale(context)}",
JSONObject().apply {
put("alwaysAutoSignIn", false)
put("autoSignIn", false)
put("gamerTagIsDefault", true)
put("gamerTagIsExplicitlySet", false)
put("gamesLitePlayerStatsEnabled", false)
put("profileDiscoverableViaGoogleAccount", false)
put("profileVisibilityWasChosenByPlayer", false)
put("profileVisible", false)
put("gamerTag", result.getString("gamerTag"))
if (result.has("stockGamerAvatarUrl")) put("stockGamerAvatarUrl", result.getString("stockGamerAvatarUrl"))
},
{ continuation.resume(it) },
{ continuation.resumeWithException(RuntimeException(it)) }) {
override fun getHeaders(): MutableMap<String, String> {
return mutableMapOf(
"Content-Type" to "application/json; charset=utf-8",
"Authorization" to "OAuth $authToken",
"X-Device-ID" to androidId.toString(16)
)
}
}
playersClient.UpdateProfileSettingsFirstParty().execute(
UpdateProfileSettingsFirstPartyRequest(locale = locale, profile_settings = profileSettings)
)
}
}
Expand Down Expand Up @@ -304,17 +288,25 @@ suspend fun performGamesSignIn(
if (!GameProfileSettings.getAllowCreatePlayer(context)) {
return false
}
registerForGames(context, account, queue)
registerForGames(context, account)
fetchSelfPlayer(context, authResponse.auth, queue)
} catch (e : Exception){
requestGameToken(context, account, scopes, authManager.isPermitted)?.let {
fetchSelfPlayer(context, it, queue)
try {
fetchSelfPlayer(context, it, queue)
} catch (e: Exception) {
return false
}
} ?: return false
}
}
403 -> {
requestGameToken(context, account, scopes, authManager.isPermitted)?.let {
fetchSelfPlayer(context, it, queue)
try {
fetchSelfPlayer(context, it, queue)
} catch (e: Exception) {
return false
}
} ?: return false
}
else -> throw e
Expand All @@ -336,19 +328,19 @@ suspend fun fetchSelfPlayer(
context: Context,
authToken: String,
queue: RequestQueue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) }
) = suspendCoroutine<JSONObject> { continuation ->
queue.add(
object : JsonObjectRequest(
"https://www.googleapis.com/games/v1/players/me",
{ continuation.resume(it) },
{ continuation.resumeWithException(it) }) {
override fun getHeaders(): MutableMap<String, String> {
return mutableMapOf(
"Authorization" to "OAuth $authToken"
)
}
) = suspendCancellableCoroutine<JSONObject> { continuation ->
val request = object : JsonObjectRequest(
"https://www.googleapis.com/games/v1/players/me",
{ if (continuation.isActive) continuation.resume(it) },
{ if (continuation.isActive) continuation.resumeWithException(it) }) {
override fun getHeaders(): MutableMap<String, String> {
return mutableMapOf(
"Authorization" to "OAuth $authToken"
)
}
)
}
continuation.invokeOnCancellation { request.cancel() }
queue.add(request)
}

suspend fun requestGameToken(
Expand Down