From 40269aba08d5b6139a0493d0dd0edc9097368f97 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Sat, 9 Sep 2023 15:20:13 +0200 Subject: [PATCH] Games: Add initial basic support for Play Games This puts the base to allow apps that allow/require sign in with Google Play Games to work. Note that as the Play Games companion app is not functional (yet), many features of Play Games remain unusable. --- .../gms/games/internal/IGamesCallbacks.aidl | 47 -- .../gms/games/internal/IGamesService.aidl | 80 --- .../multiplayer/realtime/RealTimeMessage.java | 75 --- .../gms/auth/api/signin/SignInAccount.java | 8 + .../signin/internal/SignInConfiguration.java | 8 + .../microg/gms/AbstractGmsServiceBroker.java | 7 +- .../gms/signin/internal/ISignInCallbacks.aidl | 2 +- .../auth/api/signin/GoogleSignInAccount.java | 18 + .../auth/api/signin/GoogleSignInOptions.java | 15 + .../gms/common/data/AbstractDataBuffer.java | 88 +++ .../android/gms/common/data/DataBuffer.java | 68 +-- .../gms/common/data/DataBufferIterator.java | 40 ++ .../gms/common/data/DataBufferRef.java | 71 +++ .../android/gms/common/data/DataHolder.java | 39 +- .../data/SingleRefDataBufferIterator.java | 37 ++ .../gms/common/images/ImageManager.java | 26 + .../android/gms/common/images/Size.java | 5 +- .../android/gms/common/images/WebImage.java | 99 ++- .../common/internal/IGmsServiceBroker.aidl | 28 +- .../com/google/android/gms/common/Scopes.java | 2 + .../google/android/gms/common/api/Status.java | 8 + .../common/internal/GetServiceRequest.java | 4 +- play-services-core/build.gradle | 1 + .../src/main/AndroidManifest.xml | 9 +- .../java/org/microg/gms/auth/AuthManager.java | 21 +- .../java/org/microg/gms/auth/AuthRequest.java | 16 +- .../microg/gms/auth/login/LoginActivity.java | 4 - .../gms/auth/signin/AuthSignInActivity.kt | 74 ++- .../gms/auth/signin/AuthSignInService.kt | 41 +- ...rvice.kt => SignInConfigurationService.kt} | 18 +- .../org/microg/gms/auth/signin/extensions.kt | 15 +- .../gms/games/FirstPartyGamesService.kt | 72 +++ .../gms/games/GamesConfigurationService.kt | 233 +++++++ .../microg/gms/games/GamesConnectService.kt | 96 +++ .../org/microg/gms/games/GamesService.kt | 574 ++++++++++++++++++ .../microg/gms/games/GamesSignInActivity.kt | 82 +++ .../kotlin/org/microg/gms/games/extensions.kt | 267 ++++++++ .../org/microg/gms/signin/SignInService.kt | 71 ++- play-services-games/build.gradle | 44 ++ .../src/main/AndroidManifest.xml | 6 + .../android/gms/games/PlayerEntity.aidl | 3 + .../gms/games/client/IPlayGamesCallbacks.aidl | 9 + .../gms/games/client/IPlayGamesService.aidl | 14 + .../client/PlayGamesConsistencyTokens.aidl | 3 + .../gms/games/internal/IGamesCallbacks.aidl | 49 ++ .../gms/games/internal/IGamesClient.aidl | 4 + .../gms/games/internal/IGamesService.aidl | 116 ++++ .../internal/connect/GamesSignInRequest.aidl | 3 + .../internal/connect/GamesSignInResponse.aidl | 3 + .../connect/IGamesConnectCallbacks.aidl | 8 + .../connect/IGamesConnectService.aidl | 8 + .../multiplayer/realtime/RealTimeMessage.aidl | 0 .../android/gms/games/AnnotatedData.java | 40 ++ .../android/gms/games/CurrentPlayerInfo.java | 23 + .../gms/games/CurrentPlayerInfoEntity.java | 46 ++ .../com/google/android/gms/games/Player.java | 197 ++++++ .../android/gms/games/PlayerBuffer.java | 28 + .../android/gms/games/PlayerColumns.java | 80 +++ .../android/gms/games/PlayerEntity.java | 276 +++++++++ .../google/android/gms/games/PlayerLevel.java | 73 +++ .../android/gms/games/PlayerLevelInfo.java | 87 +++ .../google/android/gms/games/PlayerRef.java | 133 ++++ .../gms/games/PlayerRelationshipInfo.java | 24 + .../games/PlayerRelationshipInfoEntity.java | 67 ++ .../com/google/android/gms/games/Players.java | 18 + .../android/gms/games/PlayersClient.java | 68 +++ .../gms/games/achievement/package-info.java | 11 + .../client/PlayGamesConsistencyTokens.java | 53 ++ .../internal/connect/GamesSignInRequest.java | 28 + .../internal/connect/GamesSignInResponse.java | 14 + .../connect/SignInResolutionResult.java | 24 + .../internal/player/MostRecentGameInfo.java | 24 + .../player/MostRecentGameInfoEntity.java | 82 +++ .../gms/games/leaderboard/package-info.java | 11 + .../multiplayer/realtime/RealTimeMessage.java | 65 ++ .../android/gms/games/package-info.java | 11 + .../gms/games/snapshot/package-info.java | 11 + .../android/gms/wearable/DataEventBuffer.java | 3 +- .../android/gms/wearable/DataItemBuffer.java | 3 +- settings.gradle | 1 + 80 files changed, 3707 insertions(+), 433 deletions(-) delete mode 100644 play-services-api/src/main/aidl/com/google/android/gms/games/internal/IGamesCallbacks.aidl delete mode 100644 play-services-api/src/main/aidl/com/google/android/gms/games/internal/IGamesService.aidl delete mode 100644 play-services-api/src/main/java/com/google/android/gms/games/multiplayer/realtime/RealTimeMessage.java create mode 100644 play-services-base/src/main/java/com/google/android/gms/common/data/AbstractDataBuffer.java create mode 100644 play-services-base/src/main/java/com/google/android/gms/common/data/DataBufferIterator.java create mode 100644 play-services-base/src/main/java/com/google/android/gms/common/data/DataBufferRef.java create mode 100644 play-services-base/src/main/java/com/google/android/gms/common/data/SingleRefDataBufferIterator.java create mode 100644 play-services-base/src/main/java/com/google/android/gms/common/images/ImageManager.java rename play-services-core/src/main/kotlin/org/microg/gms/auth/signin/{SignInDefaultService.kt => SignInConfigurationService.kt} (90%) create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/games/FirstPartyGamesService.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/games/GamesConfigurationService.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/games/GamesConnectService.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/games/GamesService.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/games/GamesSignInActivity.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/games/extensions.kt create mode 100644 play-services-games/build.gradle create mode 100644 play-services-games/src/main/AndroidManifest.xml create mode 100644 play-services-games/src/main/aidl/com/google/android/gms/games/PlayerEntity.aidl create mode 100644 play-services-games/src/main/aidl/com/google/android/gms/games/client/IPlayGamesCallbacks.aidl create mode 100644 play-services-games/src/main/aidl/com/google/android/gms/games/client/IPlayGamesService.aidl create mode 100644 play-services-games/src/main/aidl/com/google/android/gms/games/client/PlayGamesConsistencyTokens.aidl create mode 100644 play-services-games/src/main/aidl/com/google/android/gms/games/internal/IGamesCallbacks.aidl create mode 100644 play-services-games/src/main/aidl/com/google/android/gms/games/internal/IGamesClient.aidl create mode 100644 play-services-games/src/main/aidl/com/google/android/gms/games/internal/IGamesService.aidl create mode 100644 play-services-games/src/main/aidl/com/google/android/gms/games/internal/connect/GamesSignInRequest.aidl create mode 100644 play-services-games/src/main/aidl/com/google/android/gms/games/internal/connect/GamesSignInResponse.aidl create mode 100644 play-services-games/src/main/aidl/com/google/android/gms/games/internal/connect/IGamesConnectCallbacks.aidl create mode 100644 play-services-games/src/main/aidl/com/google/android/gms/games/internal/connect/IGamesConnectService.aidl rename {play-services-api => play-services-games}/src/main/aidl/com/google/android/gms/games/multiplayer/realtime/RealTimeMessage.aidl (100%) create mode 100644 play-services-games/src/main/java/com/google/android/gms/games/AnnotatedData.java create mode 100644 play-services-games/src/main/java/com/google/android/gms/games/CurrentPlayerInfo.java create mode 100644 play-services-games/src/main/java/com/google/android/gms/games/CurrentPlayerInfoEntity.java create mode 100644 play-services-games/src/main/java/com/google/android/gms/games/Player.java create mode 100644 play-services-games/src/main/java/com/google/android/gms/games/PlayerBuffer.java create mode 100644 play-services-games/src/main/java/com/google/android/gms/games/PlayerColumns.java create mode 100644 play-services-games/src/main/java/com/google/android/gms/games/PlayerEntity.java create mode 100644 play-services-games/src/main/java/com/google/android/gms/games/PlayerLevel.java create mode 100644 play-services-games/src/main/java/com/google/android/gms/games/PlayerLevelInfo.java create mode 100644 play-services-games/src/main/java/com/google/android/gms/games/PlayerRef.java create mode 100644 play-services-games/src/main/java/com/google/android/gms/games/PlayerRelationshipInfo.java create mode 100644 play-services-games/src/main/java/com/google/android/gms/games/PlayerRelationshipInfoEntity.java create mode 100644 play-services-games/src/main/java/com/google/android/gms/games/Players.java create mode 100644 play-services-games/src/main/java/com/google/android/gms/games/PlayersClient.java create mode 100644 play-services-games/src/main/java/com/google/android/gms/games/achievement/package-info.java create mode 100644 play-services-games/src/main/java/com/google/android/gms/games/client/PlayGamesConsistencyTokens.java create mode 100644 play-services-games/src/main/java/com/google/android/gms/games/internal/connect/GamesSignInRequest.java create mode 100644 play-services-games/src/main/java/com/google/android/gms/games/internal/connect/GamesSignInResponse.java create mode 100644 play-services-games/src/main/java/com/google/android/gms/games/internal/connect/SignInResolutionResult.java create mode 100644 play-services-games/src/main/java/com/google/android/gms/games/internal/player/MostRecentGameInfo.java create mode 100644 play-services-games/src/main/java/com/google/android/gms/games/internal/player/MostRecentGameInfoEntity.java create mode 100644 play-services-games/src/main/java/com/google/android/gms/games/leaderboard/package-info.java create mode 100644 play-services-games/src/main/java/com/google/android/gms/games/multiplayer/realtime/RealTimeMessage.java create mode 100644 play-services-games/src/main/java/com/google/android/gms/games/package-info.java create mode 100644 play-services-games/src/main/java/com/google/android/gms/games/snapshot/package-info.java diff --git a/play-services-api/src/main/aidl/com/google/android/gms/games/internal/IGamesCallbacks.aidl b/play-services-api/src/main/aidl/com/google/android/gms/games/internal/IGamesCallbacks.aidl deleted file mode 100644 index fa1349db4d..0000000000 --- a/play-services-api/src/main/aidl/com/google/android/gms/games/internal/IGamesCallbacks.aidl +++ /dev/null @@ -1,47 +0,0 @@ -package com.google.android.gms.games.internal; - -import com.google.android.gms.common.data.DataHolder; -import com.google.android.gms.games.multiplayer.realtime.RealTimeMessage; - -interface IGamesCallbacks { - void onAuthTokenLoaded(int statusCode, String authToken) = 5000; - void onAchievementsLoaded(in DataHolder data) = 5001; - void onAchievementUpdated(int statusCode, String achievementId) = 5002; - void onLeaderboardsLoaded(in DataHolder data) = 5003; - void onLeaderboardScoresLoaded(in DataHolder leaderboard, in DataHolder scores) = 5004; - void onScoreSubmitted(in DataHolder data) = 5005; - void onPlayersLoaded(in DataHolder data) = 5006; - void onExtendedPlayersLoaded(in DataHolder data) = 5007; - void onGamesLoaded(in DataHolder data) = 5008; - void onExtendedGamesLoaded(in DataHolder data) = 5009; - void onGameInstancesLoaded(in DataHolder data) = 5010; - void onGameplayAclLoaded(in DataHolder data) = 5011; - void onGameplayAclUpdated(int statusCode) = 5012; - void onFAclLoaded(in DataHolder data) = 5013; - void onFAclUpdated(int statusCode) = 5014; - void onSignOutComplete() = 5015; - void onInvitationsLoaded(in DataHolder data) = 5016; - void onRoomCreated(in DataHolder data) = 5017; - void onJoinedRoom(in DataHolder data) = 5018; - void onLeftRoom(int statusCode, String roomId) = 5019; - void onRoomConnecting(in DataHolder data) = 5020; - void onRoomAutoMatching(in DataHolder data) = 5021; - void onRoomConnected(in DataHolder data) = 5022; - void onConnectedToRoom(in DataHolder data) = 5023; - void onDisconnectedFromRoom(in DataHolder data) = 5024; - void onPeerInvitedToRoom(in DataHolder data, in String[] participantIds) = 5025; - void onPeerJoinedRoom(in DataHolder data, in String[] participantIds) = 5026; - void onPeerLeftRoom(in DataHolder data, in String[] participantIds) = 5027; - void onPeerDeclined(in DataHolder data, in String[] participantIds) = 5028; - void onPeerConnected(in DataHolder data, in String[] participantIds) = 5029; - void onPeerDisconnected(in DataHolder data, in String[] participantIds) = 5030; - void onRealTimeMessageReceived(in RealTimeMessage message) = 5031; - void onMessageSent(int statusCode, int messageId, String recipientParticipantId) = 5032; - void onGameMuteStatusChanged(int statusCode, String externalGameId, boolean isMuted) = 5033; - void onNotifyAclLoaded(in DataHolder data) = 5034; - void onNotifyAclUpdated(int statusCode) = 5035; - void onInvitationReceived(in DataHolder data) = 5036; - void onGameMuteStatusLoaded(in DataHolder data) = 5037; - void onContactSettingsLoaded(in DataHolder data) = 5038; - void onContactSettingsUpdated(int statusCode) = 5039; -} diff --git a/play-services-api/src/main/aidl/com/google/android/gms/games/internal/IGamesService.aidl b/play-services-api/src/main/aidl/com/google/android/gms/games/internal/IGamesService.aidl deleted file mode 100644 index 1f8e7b4e20..0000000000 --- a/play-services-api/src/main/aidl/com/google/android/gms/games/internal/IGamesService.aidl +++ /dev/null @@ -1,80 +0,0 @@ -package com.google.android.gms.games.internal; - -import android.net.Uri; -import android.os.Bundle; -import android.os.IBinder; -import com.google.android.gms.games.internal.IGamesCallbacks; -import com.google.android.gms.common.data.DataHolder; - -interface IGamesService { - void clientDisconnecting(long clientId) = 5000; - void signOut(IGamesCallbacks callbacks) = 5001; - String getAppId() = 5002; - Bundle getConnectionHint() = 5003; - void showWelcomePopup(IBinder windowToken, in Bundle extraArgs) = 5004; - void cancelPopups() = 5005; - String getCurrentAccountName() = 5006; - void loadGameplayAclInternal(IGamesCallbacks callbacks, String gameId) = 5007; - void updateGameplayAclInternal(IGamesCallbacks callbacks, String gameId, String aclData) = 5008; - void loadFAclInternal(IGamesCallbacks callbacks, String gameId) = 5009; - void updateFAclInternal(IGamesCallbacks callbacks, String gameId, boolean allCirclesVisible, in long[] circleIds) = 5010; - String getCurrentPlayerId() = 5011; - DataHolder getCurrentPlayer() = 5012; - void loadPlayer(IGamesCallbacks callbacks, String playerId) = 5013; - void loadInvitablePlayers(IGamesCallbacks callbacks, int pageSize, boolean expandCachedData, boolean forceReload) = 5014; - void submitScore(IGamesCallbacks callbacks, String leaderboardId, long score) = 5015; - void loadLeaderboards(IGamesCallbacks callbacks) = 5016; - void loadLeaderboard(IGamesCallbacks callbacks, String leaderboardId) = 5017; - void loadTopScores(IGamesCallbacks callbacks, String leaderboardId, int span, int leaderboardCollection, int maxResults, boolean forceReload) = 5018; - void loadPlayerCenteredScores(IGamesCallbacks callbacks, String leaderboardId, int span, int leaderboardCollection, int maxResults, boolean forceReload) = 5019; - void loadMoreScores(IGamesCallbacks callbacks, in Bundle previousheader, int maxResults, int pageDirection) = 5020; - void loadAchievements(IGamesCallbacks callbacks) = 5021; - void revealAchievement(IGamesCallbacks callbacks, String achievementId, IBinder windowToken, in Bundle extraArgs) = 5022; - void unlockAchievement(IGamesCallbacks callbacks, String achievementId, IBinder windowToken, in Bundle extraArgs) = 5023; - void incrementAchievement(IGamesCallbacks callbacks, String achievementId, int numSteps, IBinder windowToken, in Bundle extraArgs) = 5024; - void loadGame(IGamesCallbacks callbacks) = 5025; - void loadInvitations(IGamesCallbacks callbacks) = 5026; - void declineInvitation(String invitationId, int invitationType) = 5027; - void dismissInvitation(String invitationId, int invitationType) = 5028; - void createRoom(IGamesCallbacks callbacks, IBinder processBinder, int variant, in String[] invitedPlayerIds, in Bundle autoMatchCriteria, boolean enableSockets, long clientId) = 5029; - void joinRoom(IGamesCallbacks callbacks, IBinder processBinder, String matchId, boolean enableSockets, long clientId) = 5030; - void leaveRoom(IGamesCallbacks callbacks, String matchId) = 5031; - int sendReliableMessage(IGamesCallbacks callbacks, in byte[] messageData, String matchId, String recipientParticipantId) = 5032; - int sendUnreliableMessage(in byte[] messageData, String matchId, in String[] recipientParticipantIds) = 5033; - String createSocketConnection(String participantId) = 5034; - void clearNotifications(int notificationTypes) = 5035; - void loadLeaderboardsFirstParty(IGamesCallbacks callbacks, String gameId) = 5036; - void loadLeaderboardFirstParty(IGamesCallbacks callbacks, String gameId, String leaderboardId) = 5037; - void loadTopScoresFirstParty(IGamesCallbacks callbacks, String gameId, String leaderboardId, int span, int leaderboardCollection, int maxResults, boolean forceReload) = 5038; - void loadPlayerCenteredScoresFirstParty(IGamesCallbacks callbacks, String gameId, String leaderboardId, int span, int leaderboardCollection, int maxResults, boolean forceReload) = 5039; - void loadAchievementsFirstParty(IGamesCallbacks callbacks, String playerId, String gameId) = 5040; - void loadGameFirstParty(IGamesCallbacks callbacks, String gameId) = 5041; - void loadGameInstancesFirstParty(IGamesCallbacks callbacks, String gameId) = 5042; - void loadGameCollectionFirstParty(IGamesCallbacks callbacks, int pageSize, int collectionType, boolean expandCachedData, boolean forceReload) = 5043; - void loadRecentlyPlayedGamesFirstParty(IGamesCallbacks callbacks, String externalPlayerId, int pageSize, boolean expandCachedData, boolean forceReload) = 5044; - void loadInvitablePlayersFirstParty(IGamesCallbacks callbacks, int pageSize, boolean expandCachedData, boolean forceReload) = 5045; - void loadRecentPlayersFirstParty(IGamesCallbacks callbacks) = 5046; - void loadCircledPlayersFirstParty(IGamesCallbacks callbacks, int pageSize, boolean expandCachedData, boolean forceReload) = 5047; - void loadSuggestedPlayersFirstParty(IGamesCallbacks callbacks) = 5048; - void dismissPlayerSuggestionFirstParty(String playerIdToDismiss) = 5049; - void declineInvitationFirstParty(String gameId, String invitationId, int invitationType) = 5050; - void loadInvitationsFirstParty(IGamesCallbacks callbacks, String gameId) = 5051; - int registerWaitingRoomListenerRestricted(IGamesCallbacks callbacks, String roomId) = 5052; - void setGameMuteStatusInternal(IGamesCallbacks callbacks, String gameId, boolean muted) = 5053; - void clearNotificationsFirstParty(String gameId, int notificationTypes) = 5054; - void loadNotifyAclInternal(IGamesCallbacks callbacks) = 5055; - void updateNotifyAclInternal(IGamesCallbacks callbacks, String aclData) = 5056; - void registerInvitationListener(IGamesCallbacks callbacks, long clientId) = 5057; - void unregisterInvitationListener(long clientId) = 5058; - int unregisterWaitingRoomListenerRestricted(String roomId) = 5059; - void isGameMutedInternal(IGamesCallbacks callbacks, String gameId) = 5060; - void loadContactSettingsInternal(IGamesCallbacks callbacks) = 5061; - void updateContactSettingsInternal(IGamesCallbacks callbacks, boolean enableMobileNotifications) = 5062; - String getSelectedAccountForGameFirstParty(String gamePackageName) = 5063; - void updateSelectedAccountForGameFirstParty(String gamePackageName, String accountName) = 5064; - Uri getGamesContentUriRestricted(String gameId) = 5065; - boolean shouldUseNewPlayerNotificationsFirstParty() = 5066; - void setUseNewPlayerNotificationsFirstParty(boolean newPlayerStyle) = 5067; - void searchForPlayersFirstParty(IGamesCallbacks callbacks, String query, int pageSize, boolean expandCachedData, boolean forceReload) = 5500; - DataHolder getCurrentGame() = 5501; -} diff --git a/play-services-api/src/main/java/com/google/android/gms/games/multiplayer/realtime/RealTimeMessage.java b/play-services-api/src/main/java/com/google/android/gms/games/multiplayer/realtime/RealTimeMessage.java deleted file mode 100644 index f6716de39e..0000000000 --- a/play-services-api/src/main/java/com/google/android/gms/games/multiplayer/realtime/RealTimeMessage.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (C) 2013-2019 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.android.gms.games.multiplayer.realtime; - -import android.os.Parcel; -import android.os.Parcelable; -import android.os.Parcelable.Creator; - -public final class RealTimeMessage implements Parcelable { - public static final int RELIABLE = 1; - public static final int UNRELIABLE = 0; - - private final String mSenderParticipantId; - private final byte[] mMessageData; - private final int mIsReliable; - - public RealTimeMessage(String senderParticipantId, byte[] messageData, int isReliable) { - this.mSenderParticipantId = senderParticipantId; - this.mMessageData = messageData.clone(); - this.mIsReliable = isReliable; - } - - private RealTimeMessage(Parcel parcel) { - this(parcel.readString(), parcel.createByteArray(), parcel.readInt()); - } - - public static final Creator CREATOR = new Creator() { - @Override - public RealTimeMessage createFromParcel(Parcel in) { - return new RealTimeMessage(in); - } - @Override - public RealTimeMessage[] newArray(int size) { - return new RealTimeMessage[size]; - } - }; - - public byte[] getMessageData() { - return this.mMessageData; - } - - public String getSenderParticipantId() { - return this.mSenderParticipantId; - } - - public boolean isReliable() { - return this.mIsReliable == RELIABLE; - } - - @Override - public void writeToParcel(Parcel parcel, int flag) { - parcel.writeString(this.mSenderParticipantId); - parcel.writeByteArray(this.mMessageData); - parcel.writeInt(this.mIsReliable); - } - - @Override - public int describeContents() { - return 0; - } -} diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/signin/SignInAccount.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/signin/SignInAccount.java index 2de3a3fad4..b49a17e933 100644 --- a/play-services-auth/src/main/java/com/google/android/gms/auth/api/signin/SignInAccount.java +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/signin/SignInAccount.java @@ -5,7 +5,9 @@ package com.google.android.gms.auth.api.signin; +import androidx.annotation.NonNull; import org.microg.gms.common.Hide; +import org.microg.gms.utils.ToStringHelper; import org.microg.safeparcel.AutoSafeParcelable; @Hide @@ -17,5 +19,11 @@ public class SignInAccount extends AutoSafeParcelable { @Field(8) public String userId; + @NonNull + @Override + public String toString() { + return ToStringHelper.name("SignInAccount").field("email", email).field("account", googleSignInAccount).field("userId", userId).end(); + } + public static final Creator CREATOR = findCreator(SignInAccount.class); } diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/signin/internal/SignInConfiguration.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/signin/internal/SignInConfiguration.java index 98a2f6c0ea..7ec353425d 100644 --- a/play-services-auth/src/main/java/com/google/android/gms/auth/api/signin/internal/SignInConfiguration.java +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/signin/internal/SignInConfiguration.java @@ -5,8 +5,10 @@ package com.google.android.gms.auth.api.signin.internal; +import androidx.annotation.NonNull; import com.google.android.gms.auth.api.signin.GoogleSignInOptions; import org.microg.gms.common.Hide; +import org.microg.gms.utils.ToStringHelper; import org.microg.safeparcel.AutoSafeParcelable; @Hide @@ -16,5 +18,11 @@ public class SignInConfiguration extends AutoSafeParcelable { @Field(5) public GoogleSignInOptions options; + @NonNull + @Override + public String toString() { + return ToStringHelper.name("SignInConfiguration").field("packageName", packageName).field("options", options).end(); + } + public static final Creator CREATOR = findCreator(SignInConfiguration.class); } diff --git a/play-services-base/core/src/main/java/org/microg/gms/AbstractGmsServiceBroker.java b/play-services-base/core/src/main/java/org/microg/gms/AbstractGmsServiceBroker.java index da1def3eda..cfcce5dba6 100644 --- a/play-services-base/core/src/main/java/org/microg/gms/AbstractGmsServiceBroker.java +++ b/play-services-base/core/src/main/java/org/microg/gms/AbstractGmsServiceBroker.java @@ -24,10 +24,7 @@ import android.util.Log; import com.google.android.gms.common.api.Scope; -import com.google.android.gms.common.internal.GetServiceRequest; -import com.google.android.gms.common.internal.IGmsCallbacks; -import com.google.android.gms.common.internal.IGmsServiceBroker; -import com.google.android.gms.common.internal.ValidateAccountRequest; +import com.google.android.gms.common.internal.*; import org.microg.gms.common.GmsService; @@ -108,7 +105,7 @@ public void getGamesService(IGmsCallbacks callback, int versionCode, String pack Bundle extras = params == null ? new Bundle() : params; extras.putString("com.google.android.gms.games.key.gamePackageName", gamePackageName); extras.putString("com.google.android.gms.games.key.desiredLocale", desiredLocale); - //extras.putParcelable("com.google.android.gms.games.key.popupWindowToken", popupWindowToken); + extras.putParcelable("com.google.android.gms.games.key.popupWindowToken", new BinderWrapper(popupWindowToken)); callGetService(GmsService.GAMES, callback, versionCode, packageName, extras, accountName, scopes); } diff --git a/play-services-base/src/main/aidl/com/google/android/gms/signin/internal/ISignInCallbacks.aidl b/play-services-base/src/main/aidl/com/google/android/gms/signin/internal/ISignInCallbacks.aidl index 0eff34074e..3f54a0c3ac 100644 --- a/play-services-base/src/main/aidl/com/google/android/gms/signin/internal/ISignInCallbacks.aidl +++ b/play-services-base/src/main/aidl/com/google/android/gms/signin/internal/ISignInCallbacks.aidl @@ -13,5 +13,5 @@ interface ISignInCallbacks { void onRecordConsent(in Status status) = 5; void onCurrentAccount(in Status status, in GoogleSignInAccount account) = 6; void onSignIn(in SignInResponse response) = 7; - void onRecrodConsentByConsent(in RecordConsentByConsentResultResponse response) = 8; + void onRecordConsentByConsent(in RecordConsentByConsentResultResponse response) = 8; } \ No newline at end of file diff --git a/play-services-base/src/main/java/com/google/android/gms/auth/api/signin/GoogleSignInAccount.java b/play-services-base/src/main/java/com/google/android/gms/auth/api/signin/GoogleSignInAccount.java index 54cb3e8e42..e28eb13ba2 100644 --- a/play-services-base/src/main/java/com/google/android/gms/auth/api/signin/GoogleSignInAccount.java +++ b/play-services-base/src/main/java/com/google/android/gms/auth/api/signin/GoogleSignInAccount.java @@ -18,6 +18,7 @@ import org.json.JSONObject; import org.microg.gms.auth.AuthConstants; import org.microg.gms.common.Hide; +import org.microg.gms.utils.ToStringHelper; import org.microg.safeparcel.AutoSafeParcelable; import java.util.ArrayList; @@ -260,5 +261,22 @@ public int hashCode() { return (obfuscatedIdentifier.hashCode() + 527) * 31 + getGrantedScopes().hashCode(); } + @NonNull + @Override + public String toString() { + return ToStringHelper.name("GoogleSignInAccount") + .field("id", id) + .field("tokenId", tokenId) + .field("email", email) + .field("displayName", displayName) + .field("givenName", givenName) + .field("familyName", familyName) + .field("photoUrl", photoUrl) + .field("serverAuthCode", serverAuthCode) + .field("expirationTime", expirationTime) + .field("obfuscatedIdentifier", obfuscatedIdentifier) + .end(); + } + public static final Creator CREATOR = new AutoCreator<>(GoogleSignInAccount.class); } diff --git a/play-services-base/src/main/java/com/google/android/gms/auth/api/signin/GoogleSignInOptions.java b/play-services-base/src/main/java/com/google/android/gms/auth/api/signin/GoogleSignInOptions.java index 4d2ed31a94..6033bdbbaa 100644 --- a/play-services-base/src/main/java/com/google/android/gms/auth/api/signin/GoogleSignInOptions.java +++ b/play-services-base/src/main/java/com/google/android/gms/auth/api/signin/GoogleSignInOptions.java @@ -19,6 +19,7 @@ import org.json.JSONObject; import org.microg.gms.auth.AuthConstants; import org.microg.gms.common.Hide; +import org.microg.gms.utils.ToStringHelper; import org.microg.safeparcel.AutoSafeParcelable; import java.util.*; @@ -357,5 +358,19 @@ public String toJson() { } } + @NonNull + @Override + public String toString() { + return ToStringHelper.name("GoogleSignInOptions") + .field("scopes", scopes) + .field("account", account) + .field("idTokenRequested", idTokenRequested) + .field("forceCodeForRefreshToken", forceCodeForRefreshToken) + .field("serverAuthCodeRequested", serverAuthCodeRequested) + .field("serverClientId", serverClientId) + .field("hostedDomain", hostedDomain) + .end(); + } + public static final Creator CREATOR = findCreator(GoogleSignInOptions.class); } diff --git a/play-services-base/src/main/java/com/google/android/gms/common/data/AbstractDataBuffer.java b/play-services-base/src/main/java/com/google/android/gms/common/data/AbstractDataBuffer.java new file mode 100644 index 0000000000..211aee1db3 --- /dev/null +++ b/play-services-base/src/main/java/com/google/android/gms/common/data/AbstractDataBuffer.java @@ -0,0 +1,88 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.common.data; + +import org.microg.gms.common.Hide; + +import java.util.Iterator; + +/** + * Default implementation of DataBuffer. An {@code AbstractDataBuffer} wraps data provided across the binder from Google Play services. + */ +public abstract class AbstractDataBuffer implements DataBuffer { + protected final DataHolder dataHolder; + + @Hide + public AbstractDataBuffer(DataHolder dataHolder) { + this.dataHolder = dataHolder; + } + + /** + * Releases the data buffer, for use in try-with-resources. + *

+ * Both close and release shall have identical semantics, and are idempotent. + */ + @Override + public void close() { + release(); + } + + /** + * Get the item at the specified position. Note that the objects returned from subsequent invocations of this method for the + * same position may not be identical objects, but will be equal in value. In other words: + *

+ * {@code buffer.get(i) == buffer.get(i)} may return false. + *

+ * {@code buffer.get(i).equals(buffer.get(i))} will return true. + * + * @param position The position of the item to retrieve. + * @return the item at {@code position} in this buffer. + */ + public abstract T get(int position); + + @Override + public int getCount() { + if (dataHolder == null) return 0; + return dataHolder.getCount(); + } + + /** + * @deprecated {@link #release()} and {@link #close()} are idempotent, and so is safe to call multiple times + */ + @Deprecated + @Override + public boolean isClosed() { + if (dataHolder == null) return true; + return dataHolder.isClosed(); + } + + @Override + public Iterator iterator() { + return new DataBufferIterator(this); + } + + /** + * Releases resources used by the buffer. This method is idempotent. + */ + @Override + public void release() { + if (dataHolder != null) dataHolder.close(); + } + + /** + * In order to use this you should correctly override DataBufferRef.setDataRow(int) in your DataBufferRef implementation. + * Be careful: there will be single DataBufferRef while iterating. If you are not sure - DO NOT USE this iterator. + * + * @see SingleRefDataBufferIterator + */ + @Override + public Iterator singleRefIterator() { + return new SingleRefDataBufferIterator(this); + } +} diff --git a/play-services-base/src/main/java/com/google/android/gms/common/data/DataBuffer.java b/play-services-base/src/main/java/com/google/android/gms/common/data/DataBuffer.java index 309a667bec..e502c8f0a6 100644 --- a/play-services-base/src/main/java/com/google/android/gms/common/data/DataBuffer.java +++ b/play-services-base/src/main/java/com/google/android/gms/common/data/DataBuffer.java @@ -1,17 +1,9 @@ /* - * Copyright (C) 2013-2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * SPDX-FileCopyrightText: 2015 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. */ package com.google.android.gms.common.data; @@ -23,67 +15,43 @@ import java.util.Iterator; /** - * TODO + * Interface for a buffer of typed data. */ -@PublicApi -public abstract class DataBuffer implements Releasable, Iterable { - - private DataHolder dataHolder; - - @PublicApi(exclude = true) - public DataBuffer(DataHolder dataHolder) { - this.dataHolder = dataHolder; - } +public interface DataBuffer extends Releasable, Iterable { /** - * @deprecated use {@link #release()} instead + * Releases the data buffer, for use in try-with-resources. + *

+ * Both close and release shall have identical semantics, and are idempotent. */ - @Deprecated - public final void close() { - release(); - } + void close(); /** - * Get the item at the specified position. Note that the objects returned from subsequent - * invocations of this method for the same position may not be identical objects, but will be - * equal in value. - * - * @param position The position of the item to retrieve. - * @return the item at {@code position} in this buffer. + * Returns an element on specified position. */ - public abstract T get(int position); + T get(int position); - public int getCount() { - return dataHolder == null ? 0 : dataHolder.getCount(); - } + int getCount(); /** * @deprecated {@link #release()} is idempotent, and so is safe to call multiple times */ @Deprecated - public boolean isClosed() { - return false; - } + boolean isClosed(); @Override - public Iterator iterator() { - return null; - } + Iterator iterator(); /** * Releases resources used by the buffer. This method is idempotent. */ @Override - public void release() { - - } + void release(); /** * In order to use this one should correctly override setDataRow(int) in his DataBufferRef * implementation. Be careful: there will be single DataBufferRef while iterating. * If you are not sure - DO NOT USE this iterator. */ - public Iterator singleRefIterator() { - return null; - } + Iterator singleRefIterator(); } diff --git a/play-services-base/src/main/java/com/google/android/gms/common/data/DataBufferIterator.java b/play-services-base/src/main/java/com/google/android/gms/common/data/DataBufferIterator.java new file mode 100644 index 0000000000..f12180d20b --- /dev/null +++ b/play-services-base/src/main/java/com/google/android/gms/common/data/DataBufferIterator.java @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.common.data; + +import androidx.annotation.NonNull; +import org.microg.gms.common.Hide; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +@Hide +public class DataBufferIterator implements Iterator { + protected DataBuffer dataBuffer; + protected int position = -1; + + public DataBufferIterator(@NonNull DataBuffer dataBuffer) { + this.dataBuffer = dataBuffer; + } + + @Override + public T next() { + if (!hasNext()) { + throw new NoSuchElementException("Cannot advance the iterator beyond " + position); + } + return dataBuffer.get(++position); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Cannot remove elements from a DataBufferIterator"); + } + + @Override + public boolean hasNext() { + return this.position < this.dataBuffer.getCount() - 1; + } +} diff --git a/play-services-base/src/main/java/com/google/android/gms/common/data/DataBufferRef.java b/play-services-base/src/main/java/com/google/android/gms/common/data/DataBufferRef.java new file mode 100644 index 0000000000..3c9b8cf107 --- /dev/null +++ b/play-services-base/src/main/java/com/google/android/gms/common/data/DataBufferRef.java @@ -0,0 +1,71 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.common.data; + +import android.database.CharArrayBuffer; +import androidx.annotation.NonNull; +import org.microg.gms.common.Hide; + +@Hide +public abstract class DataBufferRef { + protected DataHolder dataHolder; + protected int dataRow; + private int windowIndex; + + public DataBufferRef(DataHolder dataHolder, int dataRow) { + this.dataHolder = dataHolder; + setDataRow(dataRow); + } + + protected void copyToBuffer(@NonNull String column, @NonNull CharArrayBuffer dataOut) { + dataHolder.copyToBuffer(column, dataRow, windowIndex, dataOut); + } + + protected boolean getBoolean(@NonNull String column) { + return dataHolder.getBoolean(column, dataRow, windowIndex); + } + + protected byte[] getByteArray(@NonNull String column) { + return dataHolder.getByteArray(column, dataRow, windowIndex); + } + + protected double getDouble(@NonNull String column) { + return dataHolder.getDouble(column, dataRow, windowIndex); + } + + protected float getFloat(@NonNull String column) { + return dataHolder.getFloat(column, dataRow, windowIndex); + } + + protected int getInteger(@NonNull String column) { + return dataHolder.getInteger(column, dataRow, windowIndex); + } + + protected long getLong(@NonNull String column) { + return dataHolder.getLong(column, dataRow, windowIndex); + } + + protected String getString(@NonNull String column) { + return dataHolder.getString(column, dataRow, windowIndex); + } + + protected boolean hasColumn(@NonNull String column) { + return dataHolder.hasColumn(column); + } + + protected boolean hasNull(@NonNull String column) { + return dataHolder.hasNull(column, dataRow, windowIndex); + } + + public boolean isDataValid() { + return !this.dataHolder.isClosed(); + } + + public void setDataRow(int dataRow) { + this.dataRow = dataRow; + this.windowIndex = dataHolder.getWindowIndex(dataRow); + } +} diff --git a/play-services-base/src/main/java/com/google/android/gms/common/data/DataHolder.java b/play-services-base/src/main/java/com/google/android/gms/common/data/DataHolder.java index 79902d34c1..a03dfde746 100644 --- a/play-services-base/src/main/java/com/google/android/gms/common/data/DataHolder.java +++ b/play-services-base/src/main/java/com/google/android/gms/common/data/DataHolder.java @@ -63,6 +63,8 @@ public class DataHolder extends AutoSafeParcelable implements Closeable { private boolean closed = false; private Map columnIndices; + private int[] windowStartPositions; + private int count; protected static final int FIELD_TYPE_NULL = 0; protected static final int FIELD_TYPE_INTEGER = 1; @@ -335,13 +337,15 @@ public byte[] getByteArray(String column, int row, int windowIndex) { * @return the number of rows in the data holder. */ public int getCount() { - int c = 0; - if (windows != null) { - for (CursorWindow window : windows) { - c += window.getNumRows(); - } - } - return c; + return count; + } + + public double getDouble(String column, int row, int windowIndex) { + return windows[windowIndex].getDouble(row, columnIndices.get(column)); + } + + public float getFloat(String column, int row, int windowIndex) { + return windows[windowIndex].getFloat(row, columnIndices.get(column)); } /** @@ -390,6 +394,10 @@ public String getString(String column, int row, int windowIndex) { return windows[windowIndex].getString(row, columnIndices.get(column)); } + public boolean hasColumn(String column) { + return columnIndices.values().contains(column); + } + /** * Returns whether the given column at the provided position contains null. * This will throw an {@link IllegalArgumentException} if the column does not exist, the @@ -400,7 +408,7 @@ public String getString(String column, int row, int windowIndex) { * @param windowIndex Index of the cursor window to extract the data from. * @return Whether the column value is null at this position. */ - public boolean isNull(String column, int row, int windowIndex) { + public boolean hasNull(String column, int row, int windowIndex) { return windows[windowIndex].isNull(row, columnIndices.get(column)); } @@ -441,6 +449,21 @@ public void validateContents() { for (int i = 0; i < columns.length; i++) { columnIndices.put(columns[i], i); } + windowStartPositions = new int[windows.length]; + this.count = 0; + for (int windowIndex = 0; windowIndex < windows.length; windowIndex++) { + this.windowStartPositions[windowIndex] = this.count; + this.count += this.windows[windowIndex].getNumRows() - (this.count - windows[windowIndex].getStartPosition()); + } + } + + public int getWindowIndex(int row) { + if (row < 0 || row >= count) throw new IllegalArgumentException(); + int windowIndex = 0; + for (; windowIndex < windowStartPositions.length; windowIndex++) { + if (row < windowStartPositions[windowIndex]) break; + } + return windowIndex-1; } /** diff --git a/play-services-base/src/main/java/com/google/android/gms/common/data/SingleRefDataBufferIterator.java b/play-services-base/src/main/java/com/google/android/gms/common/data/SingleRefDataBufferIterator.java new file mode 100644 index 0000000000..53da432a9b --- /dev/null +++ b/play-services-base/src/main/java/com/google/android/gms/common/data/SingleRefDataBufferIterator.java @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.common.data; + +import androidx.annotation.NonNull; +import org.microg.gms.common.Hide; + +import java.util.NoSuchElementException; + +@Hide +public class SingleRefDataBufferIterator extends DataBufferIterator { + private T element; + + public SingleRefDataBufferIterator(@NonNull DataBuffer dataBuffer) { + super(dataBuffer); + } + + @Override + public T next() { + if (!hasNext()) { + throw new NoSuchElementException("Cannot advance the iterator beyond " + position); + } + ++position; + if (position == 0) { + element = dataBuffer.get(position); + if (!(element instanceof DataBufferRef)) { + throw new IllegalStateException("DataBuffer reference of type " + element.getClass() + " is not movable"); + } + } else { + ((DataBufferRef) element).setDataRow(position); + } + return element; + } +} diff --git a/play-services-base/src/main/java/com/google/android/gms/common/images/ImageManager.java b/play-services-base/src/main/java/com/google/android/gms/common/images/ImageManager.java new file mode 100644 index 0000000000..e347fa0e8b --- /dev/null +++ b/play-services-base/src/main/java/com/google/android/gms/common/images/ImageManager.java @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.common.images; + +import android.content.Context; + +/** + * This class is used to load images from the network and handles local caching for you. + */ +public class ImageManager { + /** + * Returns a new ImageManager for loading images from the network. + * + * @param context The context used by the ImageManager. + * @return A new ImageManager. + */ + public static ImageManager create(Context context) { + throw new UnsupportedOperationException(); + } +} diff --git a/play-services-base/src/main/java/com/google/android/gms/common/images/Size.java b/play-services-base/src/main/java/com/google/android/gms/common/images/Size.java index d17f29f788..4bbf9ff916 100644 --- a/play-services-base/src/main/java/com/google/android/gms/common/images/Size.java +++ b/play-services-base/src/main/java/com/google/android/gms/common/images/Size.java @@ -1,6 +1,9 @@ /* - * SPDX-FileCopyrightText: 2020, microG Project Team + * SPDX-FileCopyrightText: 2020 microG Project Team * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. */ package com.google.android.gms.common.images; diff --git a/play-services-base/src/main/java/com/google/android/gms/common/images/WebImage.java b/play-services-base/src/main/java/com/google/android/gms/common/images/WebImage.java index c8f8f2a26a..8b29e2df4c 100644 --- a/play-services-base/src/main/java/com/google/android/gms/common/images/WebImage.java +++ b/play-services-base/src/main/java/com/google/android/gms/common/images/WebImage.java @@ -1,17 +1,9 @@ /* - * Copyright (C) 2013-2019 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * SPDX-FileCopyrightText: 2015 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. */ package com.google.android.gms.common.images; @@ -20,45 +12,86 @@ import android.net.Uri; +import org.microg.gms.common.Hide; import org.microg.safeparcel.AutoSafeParcelable; import org.microg.safeparcel.SafeParceled; +/** + * A class that represents an image that is located on a web server. + */ public class WebImage extends AutoSafeParcelable { public static final Creator CREATOR = new AutoCreator(WebImage.class); - public WebImage () { - this.uri = null; - } - - public WebImage (Uri uri) { - this.uri = uri; - } - - @SafeParceled(1) + @Field(1) private int versionCode = 1; - @SafeParceled(2) - private final Uri uri; + @Field(2) + private final Uri url; - @SafeParceled(3) - private final int width = 0; + @Field(3) + private final int width; - @SafeParceled(4) - private final int height = 0; + @Field(4) + private final int height; - public Uri getUrl() { - return uri; + @Hide + private WebImage() { + this.url = null; + this.width = 0; + this.height = 0; } - public int getWidth() { - return width; + /** + * Constructs a new {@link WebImage} with the given URL. + * + * @param url The URL of the image. + * @throws IllegalArgumentException If the URL is null or empty. + */ + public WebImage(Uri url) { + this(url, 0, 0); } + /** + * Constructs a new {@link WebImage} with the given URL and dimensions. + * + * @param url The URL of the image. + * @param width The width of the image, in pixels. + * @param height The height of the image, in pixels. + * @throws IllegalArgumentException If the URL is null or empty, or the dimensions are invalid. + */ + public WebImage(Uri url, int width, int height) { + if (url == null) throw new IllegalArgumentException("url cannot be null"); + if (width < 0 || height < 0) throw new IllegalArgumentException("width and height must not be negative"); + this.url = url; + this.width = width; + this.height = height; + } + + /** + * Gets the image height, in pixels. + */ public int getHeight() { return height; } + /** + * Gets the image URL. + */ + public Uri getUrl() { + return url; + } + + /** + * Gets the image width, in pixels. + */ + public int getWidth() { + return width; + } + + /** + * Returns a string representation of this object. + */ public String toString() { - return String.format(Locale.getDefault(), "Image %dx%d %s", new Object[]{Integer.valueOf(width), Integer.valueOf(height), uri.toString()}); + return String.format(Locale.getDefault(), "Image %dx%d %s", Integer.valueOf(width), Integer.valueOf(height), url.toString()); } } diff --git a/play-services-basement/src/main/aidl/com/google/android/gms/common/internal/IGmsServiceBroker.aidl b/play-services-basement/src/main/aidl/com/google/android/gms/common/internal/IGmsServiceBroker.aidl index 9649150d44..7ec24ebc46 100644 --- a/play-services-basement/src/main/aidl/com/google/android/gms/common/internal/IGmsServiceBroker.aidl +++ b/play-services-basement/src/main/aidl/com/google/android/gms/common/internal/IGmsServiceBroker.aidl @@ -17,20 +17,20 @@ interface IGmsServiceBroker { void getGoogleLocationManagerService(IGmsCallbacks callback, int code, String str, in Bundle params) = 7; void getGamesService(IGmsCallbacks callback, int code, String packageName, String accountName, in String[] scopes, String gamePackageName, IBinder popupWindowToken, String desiredLocale, in Bundle params) = 8; void getAppStateService(IGmsCallbacks callback, int code, String packageName, String accountName, in String[] scopes) = 9; - void getPlayLogService(IGmsCallbacks callback, int code, String str, in Bundle params) = 10; - void getAdMobService(IGmsCallbacks callback, int code, String str, in Bundle params) = 11; - void getDroidGuardService(IGmsCallbacks callback, int code, String str, in Bundle params) = 12; - void getLockboxService(IGmsCallbacks callback, int code, String str, in Bundle params) = 13; - void getCastMirroringService(IGmsCallbacks callback, int code, String str, in Bundle params) = 14; - void getNetworkQualityService(IGmsCallbacks callback, int code, String str, in Bundle params) = 15; - void getGoogleIdentityService(IGmsCallbacks callback, int code, String str, in Bundle params) = 16; - void getGoogleFeedbackService(IGmsCallbacks callback, int code, String str, in Bundle params) = 17; - void getCastService(IGmsCallbacks callback, int code, String str, IBinder binder, in Bundle params) = 18; - void getDriveService(IGmsCallbacks callback, int code, String str1, in String[] args, String str2, in Bundle params) = 19; - void getLightweightAppDataSearchService(IGmsCallbacks callback, int code, String str) = 20; - void getSearchAdministrationService(IGmsCallbacks callback, int code, String str) = 21; - void getAutoBackupService(IGmsCallbacks callback, int code, String str, in Bundle params) = 22; - void getAddressService(IGmsCallbacks callback, int code, String str) = 23; + void getPlayLogService(IGmsCallbacks callback, int code, String packageName, in Bundle params) = 10; + void getAdMobService(IGmsCallbacks callback, int code, String packageName, in Bundle params) = 11; + void getDroidGuardService(IGmsCallbacks callback, int code, String packageName, in Bundle params) = 12; + void getLockboxService(IGmsCallbacks callback, int code, String packageName, in Bundle params) = 13; + void getCastMirroringService(IGmsCallbacks callback, int code, String packageName, in Bundle params) = 14; + void getNetworkQualityService(IGmsCallbacks callback, int code, String packageName, in Bundle params) = 15; + void getGoogleIdentityService(IGmsCallbacks callback, int code, String packageName, in Bundle params) = 16; + void getGoogleFeedbackService(IGmsCallbacks callback, int code, String packageName, in Bundle params) = 17; + void getCastService(IGmsCallbacks callback, int code, String packageName, IBinder binder, in Bundle params) = 18; + void getDriveService(IGmsCallbacks callback, int code, String packageName, in String[] args, String str2, in Bundle params) = 19; + void getLightweightAppDataSearchService(IGmsCallbacks callback, int code, String packageName) = 20; + void getSearchAdministrationService(IGmsCallbacks callback, int code, String packageName) = 21; + void getAutoBackupService(IGmsCallbacks callback, int code, String packageName, in Bundle params) = 22; + void getAddressService(IGmsCallbacks callback, int code, String packageName) = 23; void getWalletServiceWithPackageName(IGmsCallbacks callback, int code, String packageName) = 41; diff --git a/play-services-basement/src/main/java/com/google/android/gms/common/Scopes.java b/play-services-basement/src/main/java/com/google/android/gms/common/Scopes.java index d8526bb1ef..5c86fb816f 100644 --- a/play-services-basement/src/main/java/com/google/android/gms/common/Scopes.java +++ b/play-services-basement/src/main/java/com/google/android/gms/common/Scopes.java @@ -60,6 +60,8 @@ public class Scopes { public static final String GAMES = "https://www.googleapis.com/auth/games"; @Hide public static final String GAMES_LITE = "https://www.googleapis.com/auth/games_lite"; + @Hide + public static final String GAMES_FIRSTPARTY = "https://www.googleapis.com/auth/games.firstparty"; /** * Scope for using the CloudSave service. */ diff --git a/play-services-basement/src/main/java/com/google/android/gms/common/api/Status.java b/play-services-basement/src/main/java/com/google/android/gms/common/api/Status.java index d5a06814f5..2b6862f121 100644 --- a/play-services-basement/src/main/java/com/google/android/gms/common/api/Status.java +++ b/play-services-basement/src/main/java/com/google/android/gms/common/api/Status.java @@ -22,7 +22,9 @@ import android.content.IntentSender; import android.content.IntentSender.SendIntentException; +import androidx.annotation.NonNull; import org.microg.gms.common.PublicApi; +import org.microg.gms.utils.ToStringHelper; import org.microg.safeparcel.AutoSafeParcelable; import org.microg.safeparcel.SafeParceled; @@ -176,5 +178,11 @@ public void startResolutionForResult(Activity activity, int requestCode) throws } } + @NonNull + @Override + public String toString() { + return ToStringHelper.name("Status").field("code", statusCode).field("message", statusMessage).field("resolution", resolution).end(); + } + public static final Creator CREATOR = new AutoCreator(Status.class); } diff --git a/play-services-basement/src/main/java/com/google/android/gms/common/internal/GetServiceRequest.java b/play-services-basement/src/main/java/com/google/android/gms/common/internal/GetServiceRequest.java index 0b1cc52f91..103cd3b444 100644 --- a/play-services-basement/src/main/java/com/google/android/gms/common/internal/GetServiceRequest.java +++ b/play-services-basement/src/main/java/com/google/android/gms/common/internal/GetServiceRequest.java @@ -55,7 +55,7 @@ public class GetServiceRequest extends AutoSafeParcelable { @Field(11) public Feature[] apiFeatures; @Field(12) - private boolean field12; + private boolean supportsConnectionInfo; @Field(13) private int field13; @Field(14) @@ -71,7 +71,7 @@ private GetServiceRequest() { public GetServiceRequest(int serviceId) { this.serviceId = serviceId; this.gmsVersion = Constants.GMS_VERSION_CODE; - this.field12 = true; + this.supportsConnectionInfo = true; } @Override diff --git a/play-services-core/build.gradle b/play-services-core/build.gradle index 624f68cc4d..5fbfc80a4b 100644 --- a/play-services-core/build.gradle +++ b/play-services-core/build.gradle @@ -54,6 +54,7 @@ dependencies { implementation project(':play-services-auth') implementation project(':play-services-clearcut') implementation project(':play-services-drive') + implementation project(':play-services-games') implementation project(':play-services-maps') implementation project(':play-services-measurement-base') implementation project(':play-services-places') diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index e2d0f728ef..e5d85458e6 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -472,18 +472,11 @@ - - - - - - - () ?: return finishResult(CommonStatusCodes.DEVELOPER_ERROR) + if (packageName == null || (packageName != callingActivity?.packageName && callingActivity?.packageName != packageName)) + return finishResult(CommonStatusCodes.DEVELOPER_ERROR, "package name mismatch") + val accountManager = getSystemService() ?: return finishResult(CommonStatusCodes.INTERNAL_ERROR, "No account manager") + + val unacceptableScopes = config?.options?.scopes.orEmpty().filter { it.scopeUri !in ACCEPTABLE_SCOPES } + if (unacceptableScopes.isNotEmpty()) return finishResult(CommonStatusCodes.DEVELOPER_ERROR, "Unacceptable scopes: $unacceptableScopes") val accounts = accountManager.getAccountsByType(DEFAULT_ACCOUNT_TYPE) if (accounts.isNotEmpty()) { @@ -74,7 +86,7 @@ class AuthSignInActivity : AppCompatActivity() { } private fun openAddAccount() { - startActivityForResult(Intent(this, LoginActivity::class.java), REQUEST_CODE_SIGN_IN) + startActivityForResult(Intent(this, LoginActivity::class.java), REQUEST_CODE_ADD_ACCOUNT) } private fun getDisplayName(account: Account): String? { @@ -165,52 +177,38 @@ class AuthSignInActivity : AppCompatActivity() { private suspend fun signIn(account: Account) { val googleSignInAccount = performSignIn(this, config?.packageName!!, config?.options, account, true) if (googleSignInAccount != null) { - finishResult(CommonStatusCodes.SUCCESS, googleSignInAccount) + finishResult(CommonStatusCodes.SUCCESS, account = account, googleSignInAccount = googleSignInAccount) } else { - finishResult(CommonStatusCodes.CANCELED) + finishResult(CommonStatusCodes.INTERNAL_ERROR, "Sign in failed") } } - private fun finishResult(statusCode: Int? = null, account: GoogleSignInAccount? = null) { + private fun finishResult(statusCode: Int, message: String? = null, account: Account? = null, googleSignInAccount: GoogleSignInAccount? = null) { val data = Intent() - data.putExtra("googleSignInStatus", statusCode?.let { Status(it) }) - data.putExtra("googleSignInAccount", account) - if (account != null) { + if (statusCode != CommonStatusCodes.SUCCESS) data.putExtra("errorCode", statusCode) + data.putExtra("googleSignInStatus", Status(statusCode, message)) + data.putExtra("googleSignInAccount", googleSignInAccount) + if (googleSignInAccount != null) { data.putExtra("signInAccount", SignInAccount().apply { - email = account.email - googleSignInAccount = account - userId = account.id + email = googleSignInAccount.email ?: account?.name + this.googleSignInAccount = googleSignInAccount + userId = googleSignInAccount.id ?: getSystemService()?.getUserData(account, "GoogleUserId") }) } - setResult(RESULT_OK, data) - val extras = data.extras - extras?.keySet() + Log.d(TAG, "Result: ${data.extras?.also { it.keySet() }}") + setResult(statusCode, data) finish() } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - REQUEST_CODE_SIGN_IN -> { - val accountManager = getSystemService() ?: return finish() - val accounts = accountManager.getAccountsByType(DEFAULT_ACCOUNT_TYPE) - if (accounts.isNotEmpty()) { - openAccountPicker(config?.packageName!!) - } else { - finishResult(CommonStatusCodes.CANCELED) - } - } - - REQUEST_CODE_PICK_ACCOUNT -> { - val accountName = data?.getStringExtra(AccountManager.KEY_ACCOUNT_NAME) - if (resultCode == RESULT_OK && accountName != null) { - val account = Account(accountName, DEFAULT_ACCOUNT_TYPE) - lifecycleScope.launchWhenStarted { - signIn(account) - } - } else { - finishResult(CommonStatusCodes.CANCELED) - } + if (requestCode == REQUEST_CODE_ADD_ACCOUNT) { + val accountManager = getSystemService() ?: return finish() + val accounts = accountManager.getAccountsByType(DEFAULT_ACCOUNT_TYPE) + if (accounts.isNotEmpty()) { + openAccountPicker(config?.packageName!!) + } else { + finishResult(CommonStatusCodes.CANCELED, "No account and creation cancelled") } } } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInService.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInService.kt index 6d3fd86b54..1ed8af4310 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInService.kt @@ -25,15 +25,19 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import com.android.volley.toolbox.JsonObjectRequest import com.android.volley.toolbox.Volley +import com.google.android.gms.auth.api.signin.GoogleSignInAccount import com.google.android.gms.auth.api.signin.GoogleSignInOptions import com.google.android.gms.auth.api.signin.internal.ISignInCallbacks import com.google.android.gms.auth.api.signin.internal.ISignInService +import com.google.android.gms.common.Feature import com.google.android.gms.common.api.CommonStatusCodes import com.google.android.gms.common.api.Scope import com.google.android.gms.common.api.Status +import com.google.android.gms.common.internal.ConnectionInfo import com.google.android.gms.common.internal.GetServiceRequest import com.google.android.gms.common.internal.IGmsCallbacks import org.microg.gms.BaseService +import org.microg.gms.auth.AuthPrefs import org.microg.gms.common.GmsService import org.microg.gms.common.PackageUtils import org.microg.gms.utils.warnOnTransactionIssues @@ -48,7 +52,9 @@ class AuthSignInService : BaseService(TAG, GmsService.AUTH_SIGN_IN) { val packageName = PackageUtils.getAndCheckCallingPackage(this, request.packageName) ?: throw IllegalArgumentException("Missing package name") val binder = AuthSignInServiceImpl(this, lifecycle, packageName, request.account, request.scopes.asList(), request.extras).asBinder() - callback.onPostInitComplete(CommonStatusCodes.SUCCESS, binder, Bundle()) + callback.onPostInitCompleteWithConnectionInfo(CommonStatusCodes.SUCCESS, binder, ConnectionInfo().apply { + features = arrayOf(Feature("user_service_account_management", 1)) + }) } } @@ -64,30 +70,40 @@ class AuthSignInServiceImpl( override fun getLifecycle(): Lifecycle = lifecycle override fun silentSignIn(callbacks: ISignInCallbacks, options: GoogleSignInOptions?) { + Log.d(TAG, "$packageName:silentSignIn($options)") + fun sendResult(account: GoogleSignInAccount?, status: Status) { + Log.d(TAG, "Result[$status]: $account") + runCatching { callbacks.onSignIn(account, status) } + } lifecycleScope.launchWhenStarted { try { - val account = account ?: options?.account ?: SignInDefaultService.getDefaultAccount(context, packageName) - if (account != null && getOAuthManager(context, packageName, options, account).isPermitted && options?.isForceCodeForRefreshToken != true) { - val googleSignInAccount = performSignIn(context, packageName, options, account) - if (googleSignInAccount != null) { - runCatching { callbacks.onSignIn(googleSignInAccount, Status(CommonStatusCodes.SUCCESS)) } + val account = account ?: options?.account ?: SignInConfigurationService.getDefaultAccount(context, packageName) + if (account != null && options?.isForceCodeForRefreshToken != true) { + if (getOAuthManager(context, packageName, options, account).isPermitted || AuthPrefs.isTrustGooglePermitted(context)) { + val googleSignInAccount = performSignIn(context, packageName, options, account) + if (googleSignInAccount != null) { + sendResult(googleSignInAccount, Status(CommonStatusCodes.SUCCESS)) + } else { + sendResult(null, Status(CommonStatusCodes.DEVELOPER_ERROR)) + } } else { - runCatching { callbacks.onSignIn(null, Status(CommonStatusCodes.DEVELOPER_ERROR)) } + sendResult(null, Status(CommonStatusCodes.SIGN_IN_REQUIRED)) } } else { - runCatching { callbacks.onSignIn(null, Status(CommonStatusCodes.SIGN_IN_REQUIRED)) } + sendResult(null, Status(CommonStatusCodes.SIGN_IN_REQUIRED)) } } catch (e: Exception) { Log.w(TAG, e) - runCatching { callbacks.onSignIn(null, Status.INTERNAL_ERROR) } + sendResult(null, Status.INTERNAL_ERROR) } } } override fun signOut(callbacks: ISignInCallbacks, options: GoogleSignInOptions?) { + Log.d(TAG, "$packageName:signOut($options)") lifecycleScope.launchWhenStarted { try { - SignInDefaultService.setDefaultAccount(context, packageName, null) + SignInConfigurationService.setDefaultAccount(context, packageName, null) runCatching { callbacks.onSignOut(Status.SUCCESS) } } catch (e: Exception) { Log.w(TAG, e) @@ -97,8 +113,9 @@ class AuthSignInServiceImpl( } override fun revokeAccess(callbacks: ISignInCallbacks, options: GoogleSignInOptions?) { + Log.d(TAG, "$packageName:revokeAccess($options)") lifecycleScope.launchWhenStarted { - val account = account ?: options?.account ?: SignInDefaultService.getDefaultAccount(context, packageName) + val account = account ?: options?.account ?: SignInConfigurationService.getDefaultAccount(context, packageName) if (account != null) { try { val authManager = getOAuthManager(context, packageName, options, account) @@ -119,7 +136,7 @@ class AuthSignInServiceImpl( authManager.invalidateAuthToken(token) authManager.isPermitted = false } - SignInDefaultService.setDefaultAccount(context, packageName, account) + SignInConfigurationService.setDefaultAccount(context, packageName, account) runCatching { callbacks.onRevokeAccess(Status.SUCCESS) } } catch (e: Exception) { Log.w(TAG, e) diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/SignInDefaultService.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/SignInConfigurationService.kt similarity index 90% rename from play-services-core/src/main/kotlin/org/microg/gms/auth/signin/SignInDefaultService.kt rename to play-services-core/src/main/kotlin/org/microg/gms/auth/signin/SignInConfigurationService.kt index f39009d976..dec280687b 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/SignInDefaultService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/SignInConfigurationService.kt @@ -9,13 +9,11 @@ import android.accounts.Account import android.accounts.AccountManager import android.app.Service import android.content.* -import android.content.Context.BIND_ABOVE_CLIENT -import android.content.Context.BIND_AUTO_CREATE import android.os.* import androidx.core.content.getSystemService import androidx.core.os.bundleOf import org.microg.gms.auth.AuthConstants -import kotlin.coroutines.Continuation +import org.microg.gms.common.PackageUtils import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine @@ -29,7 +27,7 @@ private const val MSG_SET_DEFAULT_ACCOUNT = 2 private const val MSG_DATA_PACKAGE_NAME = "package_name" private const val MSG_DATA_ACCOUNT = "account" -class SignInDefaultService : Service() { +class SignInConfigurationService : Service() { private val preferences: SharedPreferences get() = getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) private val accountManager: AccountManager @@ -68,8 +66,12 @@ class SignInDefaultService : Service() { }).binder } + private fun getPackageNameSuffix(packageName: String): String { + return packageName + ":" + PackageUtils.firstSignatureDigest(this, packageName) + } + private fun getDefaultAccount(packageName: String): Account? { - val name = preferences.getString(DEFAULT_ACCOUNT_PREFIX + packageName, null) + val name = preferences.getString(DEFAULT_ACCOUNT_PREFIX + getPackageNameSuffix(packageName), null) if (name.isNullOrBlank()) return null val accounts: Array = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE) for (account in accounts) { @@ -81,9 +83,9 @@ class SignInDefaultService : Service() { private fun setDefaultAccount(packageName: String, account: Account?) { val editor: SharedPreferences.Editor = preferences.edit() if (account == null || account.name == AuthConstants.DEFAULT_ACCOUNT) { - editor.remove(DEFAULT_ACCOUNT_PREFIX + packageName) + editor.remove(DEFAULT_ACCOUNT_PREFIX + getPackageNameSuffix(packageName)) } else { - editor.putString(DEFAULT_ACCOUNT_PREFIX + packageName, account.name) + editor.putString(DEFAULT_ACCOUNT_PREFIX + getPackageNameSuffix(packageName), account.name) } editor.apply() } @@ -112,7 +114,7 @@ class SignInDefaultService : Service() { runCatching { continuation.resumeWithException(RuntimeException("Disconnected")) } } } - val connected = context.bindService(Intent(context, SignInDefaultService::class.java), connection, BIND_AUTO_CREATE or BIND_ABOVE_CLIENT) + val connected = context.bindService(Intent(context, SignInConfigurationService::class.java), connection, BIND_AUTO_CREATE or BIND_ABOVE_CLIENT) if (!connected) { runCatching { continuation.resumeWithException(RuntimeException("Connection failed")) } runCatching { context.unbindService(connection) } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/extensions.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/extensions.kt index b3eee90663..d8658b226d 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/extensions.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/extensions.kt @@ -24,21 +24,21 @@ import kotlin.math.min private fun Long?.orMaxIfNegative() = this?.takeIf { it >= 0L } ?: Long.MAX_VALUE fun getOAuthManager(context: Context, packageName: String, options: GoogleSignInOptions?, account: Account): AuthManager { - val scopes = options?.scopes.orEmpty() + val scopes = options?.scopes.orEmpty().sortedBy { it.scopeUri } return AuthManager(context, account.name, packageName, "oauth2:${scopes.joinToString(" ")}") } suspend fun performSignIn(context: Context, packageName: String, options: GoogleSignInOptions?, account: Account, permitted: Boolean = false): GoogleSignInAccount? { val authManager = getOAuthManager(context, packageName, options, account) + if (permitted) authManager.isPermitted = true val authResponse = withContext(Dispatchers.IO) { - if (permitted) authManager.isPermitted = true authManager.requestAuth(true) } if (authResponse.auth == null) return null - val scopes = options?.scopes.orEmpty() - val includeId = scopes.any { it.scopeUri == Scopes.OPENID } - val includeEmail = scopes.any { it.scopeUri == Scopes.EMAIL } + val scopes = options?.scopes.orEmpty().sortedBy { it.scopeUri } + val includeId = scopes.any { it.scopeUri == Scopes.OPENID } || scopes.any { it.scopeUri == Scopes.GAMES_LITE } + val includeEmail = scopes.any { it.scopeUri == Scopes.EMAIL } || scopes.any { it.scopeUri == Scopes.GAMES_LITE } val includeProfile = scopes.any { it.scopeUri == Scopes.PROFILE } Log.d("AuthSignIn", "id token requested: ${options?.isIdTokenRequested == true}, serverClientId = ${options?.serverClientId}") val idTokenResponse = if (options?.isIdTokenRequested == true && options.serverClientId != null) withContext(Dispatchers.IO) { @@ -60,7 +60,6 @@ suspend fun performSignIn(context: Context, packageName: String, options: Google val googleUserId = authManager.getUserData("GoogleUserId") val id = if (includeId) googleUserId else null val tokenId = if (options?.isIdTokenRequested == true) idTokenResponse?.auth else null - val email = if (includeEmail) account.name else null val serverAuthCode: String? = if (options?.isServerAuthCodeRequested == true) serverAuthTokenResponse?.auth else null val expirationTime = min(authResponse.expiry.orMaxIfNegative(), idTokenResponse?.expiry.orMaxIfNegative()) val obfuscatedIdentifier: String = MessageDigest.getInstance("MD5").digest("$googleUserId:$packageName".encodeToByteArray()).toHexString().uppercase() @@ -80,11 +79,11 @@ suspend fun performSignIn(context: Context, packageName: String, options: Google cursor.close() } } else listOf(null, null, null, null) - SignInDefaultService.setDefaultAccount(context, packageName, account) + SignInConfigurationService.setDefaultAccount(context, packageName, account) return GoogleSignInAccount( id, tokenId, - email, + account.name, displayName, photoUrl?.let { Uri.parse(it) }, serverAuthCode, diff --git a/play-services-core/src/main/kotlin/org/microg/gms/games/FirstPartyGamesService.kt b/play-services-core/src/main/kotlin/org/microg/gms/games/FirstPartyGamesService.kt new file mode 100644 index 0000000000..c01233fcd9 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/games/FirstPartyGamesService.kt @@ -0,0 +1,72 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.games + +import android.content.Context +import android.os.Parcel +import android.util.Log +import androidx.lifecycle.Lifecycle +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.api.Status +import com.google.android.gms.common.data.DataHolder +import com.google.android.gms.common.internal.ConnectionInfo +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import com.google.android.gms.games.client.IPlayGamesCallbacks +import com.google.android.gms.games.client.IPlayGamesService +import com.google.android.gms.games.client.PlayGamesConsistencyTokens +import org.microg.gms.BaseService +import org.microg.gms.common.Constants +import org.microg.gms.common.GmsService +import org.microg.gms.common.PackageUtils +import org.microg.gms.utils.warnOnTransactionIssues + +private const val TAG = "PlayGamesService" +private val FIRST_PARTY_PACKAGES = setOf(Constants.GMS_PACKAGE_NAME, GAMES_PACKAGE_NAME) + +class FirstPartyGamesService : BaseService(TAG, GmsService.GAMES) { + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + val packageName = PackageUtils.getAndCheckCallingPackageOrExtendedAccess(this, request.packageName) + ?: throw IllegalArgumentException("Missing package name") + val callingPackageName = PackageUtils.getCallingPackage(this) ?: packageName + if (!PackageUtils.callerHasExtendedAccess(this)) throw IllegalArgumentException("$callingPackageName does not have extended access") + if (callingPackageName !in FIRST_PARTY_PACKAGES) throw IllegalArgumentException("$callingPackageName is not first-party") + callback.onPostInitCompleteWithConnectionInfo( + CommonStatusCodes.SUCCESS, + PlayGamesServiceImpl(this, lifecycle, packageName), + ConnectionInfo() + ) + } +} + +class PlayGamesServiceImpl(val context: Context, val lifecycle: Lifecycle, val packageName: String) : IPlayGamesService.Stub() { + + override fun getGameCollection(callbacks: IPlayGamesCallbacks?, maxResults: Int, gameCollectionType: Int, z: Boolean, forceReload: Boolean) { + Log.d(TAG, "Not yet implemented: getGameCollection($maxResults, $gameCollectionType, $z, $forceReload)") + callbacks?.onData(DataHolder.empty(CommonStatusCodes.SUCCESS)) + } + + override fun loadGames(callbacks: IPlayGamesCallbacks?, playerId: String?, maxResults: Int, z: Boolean, forceReload: Boolean) { + Log.d(TAG, "Not yet implemented: loadGames($playerId, $maxResults, $z, $forceReload)") + callbacks?.onData(DataHolder.empty(CommonStatusCodes.SUCCESS)) + } + + override fun getConsistencyTokens(): PlayGamesConsistencyTokens { + Log.d(TAG, "Not yet implemented: getConsistencyTokens") + return PlayGamesConsistencyTokens(null, null) + } + + override fun updateConsistencyTokens(tokens: PlayGamesConsistencyTokens?) { + Log.d(TAG, "Not yet implemented: updateConsistencyTokens($tokens)") + } + + override fun fun5041(callbacks: IPlayGamesCallbacks?) { + callbacks?.onStatus5028(Status.SUCCESS) + } + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = + warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/games/GamesConfigurationService.kt b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesConfigurationService.kt new file mode 100644 index 0000000000..10c0da6556 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesConfigurationService.kt @@ -0,0 +1,233 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.games + +import android.accounts.Account +import android.accounts.AccountManager +import android.app.Service +import android.content.* +import android.os.* +import androidx.core.content.getSystemService +import androidx.core.os.bundleOf +import org.microg.gms.auth.AuthConstants +import org.microg.gms.common.PackageUtils +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +private const val PREFERENCES_NAME = "games.config" +private const val PREF_ACCOUNT_GLOBAL = "account_global" +private const val PREF_ACCOUNT_PREFIX = "account_" +private const val PREF_PLAYER_PREFIX = "player_" + +private const val MSG_GET_DEFAULT_ACCOUNT = 1 +private const val MSG_SET_DEFAULT_ACCOUNT = 2 +private const val MSG_GET_PLAYER = 3 +private const val MSG_SET_PLAYER = 4 + +private const val MSG_DATA_PACKAGE_NAME = "package_name" +private const val MSG_DATA_ACCOUNT = "account" +private const val MSG_DATA_PLAYER = "player" + +class GamesConfigurationService : Service() { + private val preferences: SharedPreferences + get() = getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) + private val accountManager: AccountManager + get() = getSystemService()!! + + override fun onBind(intent: Intent?): IBinder { + return Messenger(object : Handler(Looper.getMainLooper()) { + override fun handleMessage(msg: Message) { + val data = when (msg.what) { + MSG_GET_DEFAULT_ACCOUNT -> { + val packageName = msg.data?.getString(MSG_DATA_PACKAGE_NAME) + val account = getDefaultAccount(packageName) + bundleOf( + MSG_DATA_PACKAGE_NAME to packageName, + MSG_DATA_ACCOUNT to account + ) + } + + MSG_SET_DEFAULT_ACCOUNT -> { + val packageName = msg.data?.getString(MSG_DATA_PACKAGE_NAME) + val account = msg.data?.getParcelable(MSG_DATA_ACCOUNT) + setDefaultAccount(packageName, account) + bundleOf( + MSG_DATA_PACKAGE_NAME to packageName, + MSG_DATA_ACCOUNT to account + ) + } + + MSG_GET_PLAYER -> { + val packageName = msg.data?.getString(MSG_DATA_PACKAGE_NAME) + val account = msg.data?.getParcelable(MSG_DATA_ACCOUNT) + val player = if (packageName != null && account != null) getPlayer(packageName, account) else null + bundleOf( + MSG_DATA_PACKAGE_NAME to packageName, + MSG_DATA_ACCOUNT to account, + MSG_DATA_PLAYER to player + ) + } + + MSG_SET_PLAYER -> { + val packageName = msg.data?.getString(MSG_DATA_PACKAGE_NAME) + val account = msg.data?.getParcelable(MSG_DATA_ACCOUNT) + val player = msg.data?.getString(MSG_DATA_PLAYER) + if (packageName != null && account != null) setPlayer(packageName, account, player) + bundleOf( + MSG_DATA_PACKAGE_NAME to packageName, + MSG_DATA_ACCOUNT to account, + MSG_DATA_PLAYER to player + ) + } + + else -> Bundle.EMPTY + } + msg.replyTo?.send(Message.obtain().also { + it.what = msg.what + it.data = data + }) + } + }).binder + } + + private fun getPackageNameSuffix(packageName: String): String { + return packageName + ":" + PackageUtils.firstSignatureDigest(this, packageName) + } + + private fun getGlobalDefaultAccount(): Account? { + val name = preferences.getString(PREF_ACCOUNT_GLOBAL, null) + val accounts: Array = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE) + for (account in accounts) { + if (account.name.equals(name)) return account + } + return null + } + + private fun getDefaultAccount(packageName: String?): Account? { + if (packageName == null) return getGlobalDefaultAccount() + val name = preferences.getString(PREF_ACCOUNT_PREFIX + getPackageNameSuffix(packageName), null) + if (name.isNullOrBlank()) return getGlobalDefaultAccount() + val accounts: Array = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE) + for (account in accounts) { + if (account.name.equals(name)) return account + } + return getGlobalDefaultAccount() + } + + private fun setDefaultAccount(packageName: String?, account: Account?) { + if (account?.name == getDefaultAccount(packageName)?.name) return + val key = if (packageName == null) PREF_ACCOUNT_GLOBAL else (PREF_ACCOUNT_PREFIX + getPackageNameSuffix(packageName)) + val editor: SharedPreferences.Editor = preferences.edit() + if (account == null || account.name == AuthConstants.DEFAULT_ACCOUNT) { + editor.remove(key) + } else { + editor.putString(key, account.name) + } + if (packageName != null) { + for (key in preferences.all.keys) { + if (key.startsWith(PREF_PLAYER_PREFIX + getPackageNameSuffix(packageName))) { + editor.remove(key) + } + } + } + editor.apply() + } + + private fun getPackageAndAccountSuffix(packageName: String, account: Account): String { + return getPackageNameSuffix(packageName) + ":" + account.name + } + + private fun getPlayer(packageName: String, account: Account): String? { + val player = preferences.getString(PREF_PLAYER_PREFIX + getPackageAndAccountSuffix(packageName, account), null) + if (player.isNullOrBlank()) return null + return player + } + + private fun setPlayer(packageName: String, account: Account, player: String?) { + val editor: SharedPreferences.Editor = preferences.edit() + if (player.isNullOrBlank()) { + editor.remove(PREF_PLAYER_PREFIX + getPackageAndAccountSuffix(packageName, account)) + } else { + editor.putString(PREF_PLAYER_PREFIX + getPackageAndAccountSuffix(packageName, account), player) + } + editor.apply() + } + + + companion object { + + private suspend fun singleRequest(context: Context, message: Message) = suspendCoroutine { continuation -> + val connection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + val connection = this + message.replyTo = Messenger(object : Handler(Looper.myLooper() ?: Looper.getMainLooper()) { + override fun handleMessage(msg: Message) { + runCatching { continuation.resume(msg) } + runCatching { context.unbindService(connection) } + } + }) + try { + Messenger(service).send(message) + } catch (e: Exception) { + runCatching { continuation.resumeWithException(e) } + runCatching { context.unbindService(connection) } + } + } + + override fun onServiceDisconnected(name: ComponentName?) { + runCatching { continuation.resumeWithException(RuntimeException("Disconnected")) } + } + } + val connected = context.bindService(Intent(context, GamesConfigurationService::class.java), connection, BIND_AUTO_CREATE or BIND_ABOVE_CLIENT) + if (!connected) { + runCatching { continuation.resumeWithException(RuntimeException("Connection failed")) } + runCatching { context.unbindService(connection) } + } + } + + suspend fun getDefaultAccount(context: Context, packageName: String?): Account? { + return singleRequest(context, Message.obtain().apply { + what = MSG_GET_DEFAULT_ACCOUNT + data = bundleOf( + MSG_DATA_PACKAGE_NAME to packageName + ) + }).data?.getParcelable(MSG_DATA_ACCOUNT) + } + + suspend fun setDefaultAccount(context: Context, packageName: String?, account: Account?) { + singleRequest(context, Message.obtain().apply { + what = MSG_SET_DEFAULT_ACCOUNT + data = bundleOf( + MSG_DATA_PACKAGE_NAME to packageName, + MSG_DATA_ACCOUNT to account + ) + }) + } + + suspend fun getPlayer(context: Context, packageName: String, account: Account): String? { + return singleRequest(context, Message.obtain().apply { + what = MSG_GET_PLAYER + data = bundleOf( + MSG_DATA_PACKAGE_NAME to packageName, + MSG_DATA_ACCOUNT to account + ) + }).data?.getString(MSG_DATA_PLAYER) + } + + suspend fun setPlayer(context: Context, packageName: String, account: Account, player: String?) { + singleRequest(context, Message.obtain().apply { + what = MSG_SET_PLAYER + data = bundleOf( + MSG_DATA_PACKAGE_NAME to packageName, + MSG_DATA_ACCOUNT to account, + MSG_DATA_PLAYER to player + ) + }) + } + + } +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/games/GamesConnectService.kt b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesConnectService.kt new file mode 100644 index 0000000000..55fc78ef51 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesConnectService.kt @@ -0,0 +1,96 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.games + +import android.accounts.Account +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Parcel +import android.util.Log +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.google.android.gms.common.Scopes +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.api.Status +import com.google.android.gms.common.internal.ConnectionInfo +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import com.google.android.gms.games.internal.connect.GamesSignInRequest +import com.google.android.gms.games.internal.connect.GamesSignInResponse +import com.google.android.gms.games.internal.connect.IGamesConnectCallbacks +import com.google.android.gms.games.internal.connect.IGamesConnectService +import org.microg.gms.BaseService +import org.microg.gms.auth.AuthManager +import org.microg.gms.auth.AuthPrefs +import org.microg.gms.common.GmsService +import org.microg.gms.common.PackageUtils +import org.microg.gms.utils.warnOnTransactionIssues +import java.util.UUID + +private const val TAG = "GamesConnectService" + +class GamesConnectService : BaseService(TAG, GmsService.GAMES) { + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + val packageName = PackageUtils.getAndCheckCallingPackage(this, request.packageName) + ?: throw IllegalArgumentException("Missing package name") + callback.onPostInitCompleteWithConnectionInfo( + CommonStatusCodes.SUCCESS, + GamesConnectServiceImpl(this, lifecycle, packageName), + ConnectionInfo() + ) + } +} + +class GamesConnectServiceImpl(val context: Context, private val lifecycle: Lifecycle, val packageName: String) : IGamesConnectService.Stub(), LifecycleOwner { + + override fun getLifecycle(): Lifecycle = lifecycle + + override fun signIn(callback: IGamesConnectCallbacks?, request: GamesSignInRequest?) { + Log.d(TAG, "signIn($request)") + fun sendSignInRequired() { + val resolution = PendingIntent.getActivity(context, packageName.hashCode(), Intent(context, GamesSignInActivity::class.java).apply { + putExtra(EXTRA_GAME_PACKAGE_NAME, packageName) + putExtra(EXTRA_SCOPES, arrayOf(Scopes.GAMES_LITE)) + }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + when (request?.signInType) { + 0 -> { // Manual sign in, provide resolution + callback?.onSignIn(Status(CommonStatusCodes.SIGN_IN_REQUIRED, null, resolution), null) + } + + 1 -> { // Auto sign-in on start, don't provide resolution if not + callback?.onSignIn(Status(CommonStatusCodes.SIGN_IN_REQUIRED), null) + } + + else -> { + callback?.onSignIn(Status(CommonStatusCodes.SIGN_IN_REQUIRED), null) + } + } + } + lifecycleScope.launchWhenStarted { + try { + val account = request?.previousStepResolutionResult?.resultData?.getParcelableExtra(EXTRA_ACCOUNT) + ?: GamesConfigurationService.getDefaultAccount(context, packageName) + ?: return@launchWhenStarted sendSignInRequired() + val authManager = AuthManager(context, account.name, packageName, "oauth2:${Scopes.GAMES_LITE}") + if (!authManager.isPermitted && !AuthPrefs.isTrustGooglePermitted(context)) return@launchWhenStarted sendSignInRequired() + val result = performGamesSignIn(context, packageName, account) + if (result) { + callback?.onSignIn(Status.SUCCESS, GamesSignInResponse().apply { gameRunToken = UUID.randomUUID().toString() }) + } else { + sendSignInRequired() + } + } catch (e: Exception) { + Log.w(TAG, e) + return@launchWhenStarted sendSignInRequired() + } + } + } + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = + warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/games/GamesService.kt b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesService.kt new file mode 100644 index 0000000000..6659a72de3 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesService.kt @@ -0,0 +1,574 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.games + +import android.accounts.Account +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.os.IBinder +import android.os.Parcel +import android.util.Log +import androidx.core.os.bundleOf +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.Scopes +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.api.Scope +import com.google.android.gms.common.api.Status +import com.google.android.gms.common.data.DataHolder +import com.google.android.gms.common.internal.ConnectionInfo +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import com.google.android.gms.games.Player +import com.google.android.gms.games.PlayerColumns +import com.google.android.gms.games.PlayerEntity +import com.google.android.gms.games.internal.IGamesCallbacks +import com.google.android.gms.games.internal.IGamesClient +import com.google.android.gms.games.internal.IGamesService +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.json.JSONObject +import org.microg.gms.BaseService +import org.microg.gms.auth.AuthConstants +import org.microg.gms.auth.AuthManager +import org.microg.gms.auth.AuthPrefs +import org.microg.gms.common.GmsService +import org.microg.gms.common.PackageUtils +import org.microg.gms.utils.warnOnTransactionIssues + +private const val TAG = "GamesService" + +class GamesService : BaseService(TAG, GmsService.GAMES) { + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + val packageName = PackageUtils.getAndCheckCallingPackageOrExtendedAccess(this, request.packageName) + ?: throw IllegalArgumentException("Missing package name") + + fun sendSignInRequired() { + Log.d(TAG, "Sending SIGN_IN_REQUIRED to $packageName") + callback.onPostInitCompleteWithConnectionInfo(ConnectionResult.SIGN_IN_REQUIRED, null, ConnectionInfo().apply { + params = bundleOf( + "pendingIntent" to PendingIntent.getActivity( + this@GamesService, + packageName.hashCode(), + Intent(this@GamesService, GamesSignInActivity::class.java).apply { + putExtra(EXTRA_GAME_PACKAGE_NAME, request.packageName) + putExtra(EXTRA_ACCOUNT, request.account) + putExtra(EXTRA_SCOPES, request.scopes) + }, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + ) + }) + } + + lifecycleScope.launchWhenStarted { + try { + val account = request.account.takeIf { it.name != AuthConstants.DEFAULT_ACCOUNT } + ?: GamesConfigurationService.getDefaultAccount(this@GamesService, packageName) + ?: return@launchWhenStarted sendSignInRequired() + + val scopes = (request.scopes.toSet() + Scope(Scopes.GAMES_LITE)).toList().sortedBy { it.scopeUri } + val authManager = AuthManager(this@GamesService, account.name, packageName, "oauth2:${scopes.joinToString(" ")}") + if (!authManager.isPermitted && !AuthPrefs.isTrustGooglePermitted(this@GamesService)) { + Log.d(TAG, "Not permitted to use $account for ${scopes.toList()}, sign in required") + return@launchWhenStarted sendSignInRequired() + } + + if (!performGamesSignIn(this@GamesService, packageName, account, scopes = scopes)) { + return@launchWhenStarted sendSignInRequired() + } + + val player = JSONObject(GamesConfigurationService.getPlayer(this@GamesService, packageName, account)).toPlayer() + + callback.onPostInitCompleteWithConnectionInfo( + CommonStatusCodes.SUCCESS, + GamesServiceImpl(this@GamesService, lifecycle, packageName, account, player), + ConnectionInfo() + ) + } catch (e: Exception) { + Log.w(TAG, e) + runCatching { callback.onPostInitCompleteWithConnectionInfo(ConnectionResult.INTERNAL_ERROR, null, null) } + } + } + } +} + +class GamesServiceImpl(val context: Context, private val lifecycle: Lifecycle, val packageName: String, val account: Account, val player: Player) : + IGamesService.Stub(), LifecycleOwner { + + override fun getLifecycle(): Lifecycle = lifecycle + + override fun clientDisconnecting(clientId: Long) { + Log.d(TAG, "Not yet implemented: clientDisconnecting($clientId)") + } + + override fun signOut(callbacks: IGamesCallbacks?) { + Log.d(TAG, "Not yet implemented: signOut") + lifecycleScope.launchWhenStarted { + GamesConfigurationService.setDefaultAccount(context, packageName, null) + callbacks?.onSignOutComplete() + } + } + + override fun getAppId(): String? { + Log.d(TAG, "Not yet implemented: getAppId") + return null + } + + override fun getConnectionHint(): Bundle? { + return null + } + + override fun showWelcomePopup(windowToken: IBinder?, extraArgs: Bundle?) { + runCatching { extraArgs?.keySet() } + Log.d(TAG, "Not yet implemented: showWelcomePopup($windowToken, $extraArgs)") + } + + override fun cancelPopups() { + Log.d(TAG, "Not yet implemented: cancelPopups") + } + + override fun getCurrentAccountName(): String? { + Log.d(TAG, "Not yet implemented: getCurrentAccountName") + return null + } + + override fun loadGameplayAclInternal(callbacks: IGamesCallbacks?, gameId: String?) { + Log.d(TAG, "Not yet implemented: loadGameplayAclInternal($gameId)") + } + + override fun updateGameplayAclInternal(callbacks: IGamesCallbacks?, gameId: String?, aclData: String?) { + Log.d(TAG, "Not yet implemented: updateGameplayAclInternal($gameId, $aclData)") + } + + override fun loadFAclInternal(callbacks: IGamesCallbacks?, gameId: String?) { + Log.d(TAG, "Not yet implemented: loadFAclInternal($gameId)") + } + + override fun updateFAclInternal(callbacks: IGamesCallbacks?, gameId: String?, allCirclesVisible: Boolean, circleIds: LongArray?) { + Log.d(TAG, "Not yet implemented: updateFAclInternal($gameId, $allCirclesVisible, $circleIds)") + } + + override fun getCurrentPlayerId(): String? { + Log.d(TAG, "Not yet implemented: getCurrentPlayerId") + return null + } + + override fun getCurrentPlayer(): DataHolder? { + return if (player is PlayerEntity) { + DataHolder.builder(PlayerColumns.CURRENT_PLAYER_COLUMNS.toTypedArray()).withRow(player.toContentValues()).build(CommonStatusCodes.SUCCESS) + } else { + DataHolder.builder(PlayerColumns.CURRENT_PLAYER_COLUMNS.toTypedArray()).build(CommonStatusCodes.SIGN_IN_REQUIRED) + } + } + + override fun loadPlayer(callbacks: IGamesCallbacks?, playerId: String?) { + Log.d(TAG, "Not yet implemented: loadPlayer($playerId)") + } + + override fun loadInvitablePlayers(callbacks: IGamesCallbacks?, pageSize: Int, expandCachedData: Boolean, forceReload: Boolean) { + Log.d(TAG, "Not yet implemented: loadInvitablePlayers($pageSize, $expandCachedData, $forceReload)") + } + + override fun submitScore(callbacks: IGamesCallbacks?, leaderboardId: String?, score: Long) { + Log.d(TAG, "Not yet implemented: submitScore($leaderboardId, $score)") + } + + override fun loadLeaderboards(callbacks: IGamesCallbacks?) { + Log.d(TAG, "Not yet implemented: loadLeaderboards") + } + + override fun loadLeaderboard(callbacks: IGamesCallbacks?, leaderboardId: String?) { + Log.d(TAG, "Not yet implemented: loadLeaderboard($leaderboardId)") + } + + override fun loadTopScores( + callbacks: IGamesCallbacks?, + leaderboardId: String?, + span: Int, + leaderboardCollection: Int, + maxResults: Int, + forceReload: Boolean + ) { + Log.d(TAG, "Not yet implemented: loadTopScores($leaderboardId, $span, $leaderboardCollection, $maxResults, $forceReload)") + } + + override fun loadPlayerCenteredScores( + callbacks: IGamesCallbacks?, + leaderboardId: String?, + span: Int, + leaderboardCollection: Int, + maxResults: Int, + forceReload: Boolean + ) { + Log.d(TAG, "Not yet implemented: loadPlayerCenteredScores($leaderboardId, $span, $leaderboardCollection, $maxResults, $forceReload)") + } + + override fun loadMoreScores(callbacks: IGamesCallbacks?, previousheader: Bundle?, maxResults: Int, pageDirection: Int) { + runCatching { previousheader?.keySet() } + Log.d(TAG, "Not yet implemented: loadMoreScores($previousheader, $maxResults, $pageDirection)") + } + + override fun loadAchievements(callbacks: IGamesCallbacks?) { + loadAchievementsV2(callbacks, false) + } + + override fun revealAchievement(callbacks: IGamesCallbacks?, achievementId: String?, windowToken: IBinder?, extraArgs: Bundle?) { + runCatching { extraArgs?.keySet() } + Log.d(TAG, "Not yet implemented: revealAchievement($achievementId, $windowToken, $extraArgs)") + } + + override fun unlockAchievement(callbacks: IGamesCallbacks?, achievementId: String?, windowToken: IBinder?, extraArgs: Bundle?) { + runCatching { extraArgs?.keySet() } + Log.d(TAG, "Not yet implemented: unlockAchievement($achievementId, $windowToken, $extraArgs") + } + + override fun incrementAchievement(callbacks: IGamesCallbacks?, achievementId: String?, numSteps: Int, windowToken: IBinder?, extraArgs: Bundle?) { + runCatching { extraArgs?.keySet() } + Log.d(TAG, "Not yet implemented: incrementAchievement($achievementId, $numSteps, $windowToken, $extraArgs)") + } + + override fun loadGame(callbacks: IGamesCallbacks?) { + Log.d(TAG, "Not yet implemented: loadGame") + } + + override fun loadInvitations(callbacks: IGamesCallbacks?) { + Log.d(TAG, "Not yet implemented: loadInvitations") + } + + override fun declineInvitation(invitationId: String?, invitationType: Int) { + Log.d(TAG, "Not yet implemented: declineInvitation($invitationId, $invitationType)") + } + + override fun dismissInvitation(invitationId: String?, invitationType: Int) { + Log.d(TAG, "Not yet implemented: dismissInvitation($invitationId, $invitationType)") + } + + override fun createRoom( + callbacks: IGamesCallbacks?, + processBinder: IBinder?, + variant: Int, + invitedPlayerIds: Array?, + autoMatchCriteria: Bundle?, + enableSockets: Boolean, + clientId: Long + ) { + Log.d(TAG, "Not yet implemented: createRoom($variant, $invitedPlayerIds, $autoMatchCriteria, $enableSockets, $clientId)") + } + + override fun joinRoom(callbacks: IGamesCallbacks?, processBinder: IBinder?, matchId: String?, enableSockets: Boolean, clientId: Long) { + Log.d(TAG, "Not yet implemented: joinRoom($matchId, $enableSockets, $clientId)") + } + + override fun leaveRoom(callbacks: IGamesCallbacks?, matchId: String?) { + Log.d(TAG, "Not yet implemented: leaveRoom($matchId)") + } + + override fun sendReliableMessage(callbacks: IGamesCallbacks?, messageData: ByteArray?, matchId: String?, recipientParticipantId: String?): Int { + Log.d(TAG, "Not yet implemented: sendReliableMessage($messageData, $matchId, $recipientParticipantId)") + return 0 + } + + override fun sendUnreliableMessage(messageData: ByteArray?, matchId: String?, recipientParticipantIds: Array?): Int { + Log.d(TAG, "Not yet implemented: sendUnreliableMessage($messageData, $matchId, $recipientParticipantIds)") + return 0 + } + + override fun createSocketConnection(participantId: String?): String? { + Log.d(TAG, "Not yet implemented: createSocketConnection($participantId)") + return null + } + + override fun clearNotifications(notificationTypes: Int) { + Log.d(TAG, "Not yet implemented: clearNotifications($notificationTypes)") + } + + override fun loadLeaderboardsFirstParty(callbacks: IGamesCallbacks?, gameId: String?) { + Log.d(TAG, "Not yet implemented: loadLeaderboardsFirstParty($gameId)") + } + + override fun loadLeaderboardFirstParty(callbacks: IGamesCallbacks?, gameId: String?, leaderboardId: String?) { + Log.d(TAG, "Not yet implemented: loadLeaderboardFirstParty($gameId, $leaderboardId)") + } + + override fun loadTopScoresFirstParty( + callbacks: IGamesCallbacks?, + gameId: String?, + leaderboardId: String?, + span: Int, + leaderboardCollection: Int, + maxResults: Int, + forceReload: Boolean + ) { + Log.d(TAG, "Not yet implemented: loadTopScoresFirstParty($gameId, $leaderboardId, $span, $leaderboardCollection, $maxResults, $forceReload)") + } + + override fun loadPlayerCenteredScoresFirstParty( + callbacks: IGamesCallbacks?, + gameId: String?, + leaderboardId: String?, + span: Int, + leaderboardCollection: Int, + maxResults: Int, + forceReload: Boolean + ) { + Log.d(TAG, "Not yet implemented: loadPlayerCenteredScoresFirstParty($gameId, $leaderboardId, $span, $leaderboardCollection, $maxResults, $forceReload)") + } + + override fun loadAchievementsFirstParty(callbacks: IGamesCallbacks?, playerId: String?, gameId: String?) { + Log.d(TAG, "Not yet implemented: loadAchievementsFirstParty($playerId, $gameId)") + } + + override fun loadGameFirstParty(callbacks: IGamesCallbacks?, gameId: String?) { + Log.d(TAG, "Not yet implemented: loadGameFirstParty($gameId)") + } + + override fun loadGameInstancesFirstParty(callbacks: IGamesCallbacks?, gameId: String?) { + Log.d(TAG, "Not yet implemented: loadGameInstancesFirstParty($gameId)") + } + + override fun loadGameCollectionFirstParty( + callbacks: IGamesCallbacks?, + pageSize: Int, + collectionType: Int, + expandCachedData: Boolean, + forceReload: Boolean + ) { + Log.d(TAG, "Not yet implemented: loadGameCollectionFirstParty($pageSize, $collectionType, $expandCachedData, $forceReload)") + } + + override fun loadRecentlyPlayedGamesFirstParty( + callbacks: IGamesCallbacks?, + externalPlayerId: String?, + pageSize: Int, + expandCachedData: Boolean, + forceReload: Boolean + ) { + Log.d(TAG, "Not yet implemented: loadRecentlyPlayedGamesFirstParty($externalPlayerId, $pageSize, $expandCachedData, $forceReload)") + } + + override fun loadInvitablePlayersFirstParty(callbacks: IGamesCallbacks?, pageSize: Int, expandCachedData: Boolean, forceReload: Boolean) { + Log.d(TAG, "Not yet implemented: loadInvitablePlayersFirstParty($pageSize, $expandCachedData, $forceReload)") + } + + override fun loadRecentPlayersFirstParty(callbacks: IGamesCallbacks?) { + Log.d(TAG, "Not yet implemented: loadRecentPlayersFirstParty") + } + + override fun loadCircledPlayersFirstParty(callbacks: IGamesCallbacks?, pageSize: Int, expandCachedData: Boolean, forceReload: Boolean) { + Log.d(TAG, "Not yet implemented: loadCircledPlayersFirstParty($pageSize, $expandCachedData, $forceReload)") + } + + override fun loadSuggestedPlayersFirstParty(callbacks: IGamesCallbacks?) { + Log.d(TAG, "Not yet implemented: loadSuggestedPlayersFirstParty") + } + + override fun dismissPlayerSuggestionFirstParty(playerIdToDismiss: String?) { + Log.d(TAG, "Not yet implemented: dismissPlayerSuggestionFirstParty($playerIdToDismiss)") + } + + override fun declineInvitationFirstParty(gameId: String?, invitationId: String?, invitationType: Int) { + Log.d(TAG, "Not yet implemented: declineInvitationFirstParty($gameId, $invitationId, $invitationType)") + } + + override fun loadInvitationsFirstParty(callbacks: IGamesCallbacks?, gameId: String?) { + Log.d(TAG, "Not yet implemented: loadInvitationsFirstParty($gameId)") + } + + override fun registerWaitingRoomListenerRestricted(callbacks: IGamesCallbacks?, roomId: String?): Int { + Log.d(TAG, "Not yet implemented: registerWaitingRoomListenerRestricted($roomId)") + return 0 + } + + override fun setGameMuteStatusInternal(callbacks: IGamesCallbacks?, gameId: String?, muted: Boolean) { + Log.d(TAG, "Not yet implemented: setGameMuteStatusInternal($gameId, $muted)") + } + + override fun clearNotificationsFirstParty(gameId: String?, notificationTypes: Int) { + Log.d(TAG, "Not yet implemented: clearNotificationsFirstParty($gameId, $notificationTypes)") + } + + override fun loadNotifyAclInternal(callbacks: IGamesCallbacks?) { + Log.d(TAG, "Not yet implemented: loadNotifyAclInternal") + } + + override fun updateNotifyAclInternal(callbacks: IGamesCallbacks?, aclData: String?) { + Log.d(TAG, "Not yet implemented: updateNotifyAclInternal($aclData)") + } + + override fun registerInvitationListener(callbacks: IGamesCallbacks?, clientId: Long) { + Log.d(TAG, "Not yet implemented: registerInvitationListener($clientId)") + } + + override fun unregisterInvitationListener(clientId: Long) { + Log.d(TAG, "Not yet implemented: unregisterInvitationListener($clientId)") + } + + override fun unregisterWaitingRoomListenerRestricted(roomId: String?): Int { + Log.d(TAG, "Not yet implemented: unregisterWaitingRoomListenerRestricted($roomId)") + return 0 + } + + override fun isGameMutedInternal(callbacks: IGamesCallbacks?, gameId: String?) { + Log.d(TAG, "Not yet implemented: isGameMutedInternal($gameId)") + } + + override fun loadContactSettingsInternal(callbacks: IGamesCallbacks?) { + Log.d(TAG, "Not yet implemented: loadContactSettingsInternal") + } + + override fun updateContactSettingsInternal(callbacks: IGamesCallbacks?, enableMobileNotifications: Boolean) { + Log.d(TAG, "Not yet implemented: updateContactSettingsInternal($enableMobileNotifications)") + } + + override fun getSelectedAccountForGameFirstParty(gamePackageName: String?): String? { + Log.d(TAG, "Not yet implemented: getSelectedAccountForGameFirstParty($gamePackageName)") + return null + } + + override fun updateSelectedAccountForGameFirstParty(gamePackageName: String?, accountName: String?) { + Log.d(TAG, "Not yet implemented: updateSelectedAccountForGameFirstParty($gamePackageName, $accountName)") + } + + override fun getGamesContentUriRestricted(gameId: String?): Uri? { + Log.d(TAG, "Not yet implemented: getGamesContentUriRestricted($gameId)") + return null + } + + override fun shouldUseNewPlayerNotificationsFirstParty(): Boolean { + Log.d(TAG, "Not yet implemented: shouldUseNewPlayerNotificationsFirstParty") + return false + } + + override fun setUseNewPlayerNotificationsFirstParty(newPlayerStyle: Boolean) { + Log.d(TAG, "Not yet implemented: setUseNewPlayerNotificationsFirstParty($newPlayerStyle)") + } + + override fun searchForPlayersFirstParty(callbacks: IGamesCallbacks?, query: String?, pageSize: Int, expandCachedData: Boolean, forceReload: Boolean) { + Log.d(TAG, "Not yet implemented: searchForPlayersFirstParty($query, $pageSize, $expandCachedData, $forceReload)") + } + + override fun getCurrentGame(): DataHolder? { + Log.d(TAG, "Not yet implemented: getCurrentGame") + return null + } + + override fun loadAchievementsV2(callbacks: IGamesCallbacks?, forceReload: Boolean) { + Log.d(TAG, "Not yet implemented: loadAchievementsV2($forceReload)") + callbacks?.onAchievementsLoaded(DataHolder.empty(CommonStatusCodes.SUCCESS)) + } + + override fun submitLeaderboardScore(callbacks: IGamesCallbacks?, leaderboardId: String?, score: Long, scoreTag: String?) { + Log.d(TAG, "Not yet implemented: submitLeaderboardScore($leaderboardId, $score, $scoreTag)") + } + + override fun setAchievementSteps(callbacks: IGamesCallbacks?, id: String?, numSteps: Int, windowToken: IBinder?, extras: Bundle?) { + runCatching { extras?.keySet() } + Log.d(TAG, "Not yet implemented: setAchievementSteps($id, $numSteps, $windowToken, $extras)") + } + + private fun getGamesIntent(action: String, block: Intent.() -> Unit = {}) = Intent(action).apply { + setPackage(GAMES_PACKAGE_NAME) + putExtra(EXTRA_GAME_PACKAGE_NAME, packageName) + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + block() + } + + override fun getAllLeaderboardsIntent(): Intent = getGamesIntent(ACTION_VIEW_LEADERBOARDS) + + override fun getAchievementsIntent(): Intent = getGamesIntent(ACTION_VIEW_ACHIEVEMENTS) + + override fun getPlayerSearchIntent(): Intent = getGamesIntent(ACTION_PLAYER_SEARCH) + + override fun loadEvents(callbacks: IGamesCallbacks?, forceReload: Boolean) { + Log.d(TAG, "Not yet implemented: loadEvents($forceReload)") + } + + override fun incrementEvent(eventId: String?, incrementAmount: Int) { + Log.d(TAG, "Not yet implemented: incrementEvent($eventId, $incrementAmount)") + } + + override fun loadEventsById(callbacks: IGamesCallbacks?, forceReload: Boolean, eventsIds: Array?) { + Log.d(TAG, "Not yet implemented: loadEventsById($forceReload, $eventsIds)") + } + + override fun getMaxDataSize(): Int { + return 3 * 1024 * 1024 + } + + override fun getMaxCoverImageSize(): Int { + return 800 * 1024 + } + + override fun registerEventClient(callback: IGamesClient?, l: Long) { + Log.d(TAG, "Not yet implemented: registerEventClient($l)") + } + + private fun getCompareProfileIntent(playerId: String, block: Intent.() -> Unit = {}): Intent = getGamesIntent(ACTION_VIEW_PROFILE) { + putExtra(EXTRA_IS_SELF, playerId == currentPlayerId) + putExtra(EXTRA_ACCOUNT, currentAccount) + block() + } + + override fun getCompareProfileIntentForPlayer(player: PlayerEntity): Intent = getCompareProfileIntent(player.playerId) { + putExtra(EXTRA_PLAYER, player) + } + + override fun loadPlayerStats(callbacks: IGamesCallbacks?, forceReload: Boolean) { + Log.d(TAG, "Not yet implemented: loadPlayerStats($forceReload)") + } + + override fun getCurrentAccount(): Account? { + Log.d(TAG, "Not yet implemented: getCurrentAccount") + return account + } + + override fun isTelevision(): Boolean { + Log.d(TAG, "Not yet implemented: isTelevision") + return false + } + + override fun getCompareProfileIntentWithAlternativeNameHints( + otherPlayerId: String, + otherPlayerInGameName: String?, + currentPlayerInGameName: String? + ): Intent = getCompareProfileIntent(otherPlayerId) { + putExtra(EXTRA_PLAYER_ID, otherPlayerId) + putExtra(EXTRA_OTHER_PLAYER_IN_GAME_NAME, otherPlayerInGameName) + putExtra(EXTRA_SELF_IN_GAME_NAME, currentPlayerInGameName) + } + + override fun requestServerSideAccess(callbacks: IGamesCallbacks, serverClientId: String, forceRefreshToken: Boolean) { + lifecycleScope.launchWhenStarted { + try { + val serverAuthTokenResponse = withContext(Dispatchers.IO) { + val serverAuthTokenManager = AuthManager(context, account.name, packageName, "oauth2:server:client_id:${serverClientId}:api_scope:${Scopes.GAMES_LITE}") + serverAuthTokenManager.setOauth2Prompt(if (forceRefreshToken) "consent" else "auto") + serverAuthTokenManager.setItCaveatTypes("2") + serverAuthTokenManager.isPermitted = true + serverAuthTokenManager.invalidateAuthToken() + serverAuthTokenManager.requestAuth(true) + } + if (serverAuthTokenResponse.auth != null) { + callbacks.onServerAuthCode(Status(CommonStatusCodes.SUCCESS), serverAuthTokenResponse.auth) + } else { + callbacks.onServerAuthCode(Status(CommonStatusCodes.SIGN_IN_REQUIRED), null) + } + } catch (e: Exception) { + Log.w(TAG, e) + runCatching { callbacks.onServerAuthCode(Status(CommonStatusCodes.INTERNAL_ERROR), null) } + } + } + } + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = + warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/games/GamesSignInActivity.kt b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesSignInActivity.kt new file mode 100644 index 0000000000..27c99cc1f3 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesSignInActivity.kt @@ -0,0 +1,82 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.games + +import android.accounts.Account +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.Gravity +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import com.google.android.gms.auth.api.signin.GoogleSignInAccount +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.auth.api.signin.internal.SignInConfiguration +import com.google.android.gms.common.api.Status +import org.microg.gms.auth.AuthConstants +import org.microg.gms.auth.signin.AuthSignInActivity +import org.microg.gms.common.Constants + +private const val TAG = "GamesSignIn" + +private const val REQUEST_CODE_GOOGLE_SIGN_IN = 200 + +class GamesSignInActivity : AppCompatActivity() { + val gamePackageName: String? + get() = intent?.getStringExtra(EXTRA_GAME_PACKAGE_NAME) + val account: Account? + get() = intent?.getParcelableExtra(EXTRA_ACCOUNT) + val popupGravity: Int + get() = intent?.getIntExtra(EXTRA_POPUP_GRAVITY, Gravity.TOP or Gravity.CENTER_HORIZONTAL) ?: (Gravity.TOP or Gravity.CENTER_HORIZONTAL) + + private val Int.px: Int get() = (this * resources.displayMetrics.density).toInt() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (gamePackageName == null || (gamePackageName != callingActivity?.packageName && callingActivity?.packageName != packageName)) return finish() + + window.setGravity(popupGravity) + startActivityForResult(Intent(this, AuthSignInActivity::class.java).apply { + putExtra("config", SignInConfiguration().apply { + packageName = gamePackageName + options = account?.name?.takeIf { it != AuthConstants.DEFAULT_ACCOUNT }?.let { + GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN).setAccountName(it).build() + } ?: GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN + }) + Log.d(TAG, "Redirect to GOOGLE_SIGN_IN using $this") + }, REQUEST_CODE_GOOGLE_SIGN_IN) + } + + private suspend fun signIn(account: Account) { + Log.d(TAG, "Sign in as $account") + if (performGamesSignIn(this, gamePackageName!!, account, permitted = true)) { + GamesConfigurationService.setDefaultAccount(this, gamePackageName, account) + } + setResult(RESULT_OK, Intent().apply { + putExtra(EXTRA_ACCOUNT, account) + }) + finish() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == REQUEST_CODE_GOOGLE_SIGN_IN) { + val status = data?.extras?.also { it.classLoader = Status::class.java.classLoader }?.getParcelable("googleSignInStatus") + if (status?.isSuccess == true) { + val account = data.extras?.also { it.classLoader = GoogleSignInAccount::class.java.classLoader } + ?.getParcelable("googleSignInAccount")?.account + if (account != null) { + lifecycleScope.launchWhenStarted { + signIn(account) + } + return + } + } + finish() + } + } +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/games/extensions.kt b/play-services-core/src/main/kotlin/org/microg/gms/games/extensions.kt new file mode 100644 index 0000000000..499c850f44 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/games/extensions.kt @@ -0,0 +1,267 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.games + +import android.accounts.Account +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import androidx.core.content.contentValuesOf +import androidx.core.net.toUri +import com.android.volley.* +import com.android.volley.Response.success +import com.android.volley.toolbox.JsonObjectRequest +import com.android.volley.toolbox.Volley +import com.google.android.gms.common.Scopes +import com.google.android.gms.common.api.Scope +import com.google.android.gms.games.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.json.JSONObject +import org.microg.gms.auth.AuthManager +import org.microg.gms.common.Constants +import org.microg.gms.common.Utils +import org.microg.gms.settings.SettingsContract.CheckIn +import org.microg.gms.settings.SettingsContract.getSettings +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + + +const val ACTION_START_1P = "com.google.android.play.games.service.START_1P" +const val ACTION_VIEW_LEADERBOARDS = "com.google.android.gms.games.VIEW_LEADERBOARDS" +const val ACTION_VIEW_ACHIEVEMENTS = "com.google.android.gms.games.VIEW_ACHIEVEMENTS" +const val ACTION_PLAYER_SEARCH = "com.google.android.gms.games.PLAYER_SEARCH" +const val ACTION_VIEW_PROFILE = "com.google.android.gms.games.VIEW_PROFILE" +const val ACTION_ADD_FRIEND = "com.google.android.gms.games.ADD_FRIEND" + +const val EXTRA_GAME_PACKAGE_NAME = "com.google.android.gms.games.GAME_PACKAGE_NAME" +const val EXTRA_GAME_ID = "com.google.android.gms.games.GAME_ID" +const val EXTRA_PLAYER = "com.google.android.gms.games.PLAYER" +const val EXTRA_PLAYER_ID = "com.google.android.gms.games.PLAYER_ID" +const val EXTRA_IS_SELF = "com.google.android.gms.games.IS_SELF" +const val EXTRA_ACCOUNT = "com.google.android.gms.games.ACCOUNT" +const val EXTRA_SCOPES = "com.google.android.gms.games.SCOPES" +const val EXTRA_POPUP_GRAVITY = "com.google.android.gms.games.key.connectingPopupGravity" +const val EXTRA_SELF_IN_GAME_NAME = "com.google.android.gms.games.EXTRA_SELF_IN_GAME_NAME" +const val EXTRA_OTHER_PLAYER_IN_GAME_NAME = "com.google.android.gms.games.EXTRA_OTHER_PLAYER_IN_GAME_NAME" + +const val GAMES_PACKAGE_NAME = "com.google.android.play.games" + +fun PlayerEntity.toContentValues(): ContentValues = contentValuesOf( + PlayerColumns.externalPlayerId to playerId, + PlayerColumns.profileName to displayName, + PlayerColumns.gamerTag to gamerTag, + PlayerColumns.realName to name, + PlayerColumns.profileIconImageUri to iconImageUri?.toString(), + PlayerColumns.profileIconImageUrl to iconImageUrl, + PlayerColumns.profileHiResImageUri to hiResImageUri?.toString(), + PlayerColumns.profileHiResImageUrl to hiResImageUrl, + PlayerColumns.bannerImageLandscapeUri to bannerImageLandscapeUri?.toString(), + PlayerColumns.bannerImageLandscapeUrl to bannerImageLandscapeUrl, + PlayerColumns.bannerImagePortraitUri to bannerImagePortraitUri?.toString(), + PlayerColumns.bannerImagePortraitUrl to bannerImagePortraitUrl, + PlayerColumns.lastUpdated to retrievedTimestamp, + PlayerColumns.isInCircles to isInCircles, + PlayerColumns.playedWithTimestamp to lastPlayedWithTimestamp, + PlayerColumns.playerTitle to title, + PlayerColumns.isProfileVisible to isProfileVisible, + PlayerColumns.hasDebugAccess to hasDebugAccess, + PlayerColumns.gamerFriendStatus to 0, + PlayerColumns.gamerFriendUpdateTimestamp to 0L, + PlayerColumns.isMuted to false, + PlayerColumns.totalUnlockedAchievements to totalUnlockedAchievement, + PlayerColumns.alwaysAutoSignIn to isAlwaysAutoSignIn, + PlayerColumns.hasAllPublicAcls to isProfileVisible, + + PlayerColumns.currentLevel to levelInfo?.currentLevel?.levelNumber, + PlayerColumns.currentLevelMinXp to levelInfo?.currentLevel?.minXp, + PlayerColumns.currentLevelMaxXp to levelInfo?.currentLevel?.maxXp, + PlayerColumns.nextLevel to levelInfo?.nextLevel?.levelNumber, + PlayerColumns.nextLevelMaxXp to levelInfo?.nextLevel?.maxXp, + PlayerColumns.lastLevelUpTimestamp to (levelInfo?.lastLevelUpTimestamp ?: -1), + PlayerColumns.currentXpTotal to (levelInfo?.currentXpTotal ?: -1L), + + PlayerColumns.mostRecentExternalGameId to mostRecentGameInfo?.gameId, + PlayerColumns.mostRecentGameName to mostRecentGameInfo?.gameName, + PlayerColumns.mostRecentActivityTimestamp to mostRecentGameInfo?.activityTimestampMillis, + PlayerColumns.mostRecentGameIconUri to mostRecentGameInfo?.gameIconImageUri?.toString(), + PlayerColumns.mostRecentGameHiResUri to mostRecentGameInfo?.gameHiResImageUri?.toString(), + PlayerColumns.mostRecentGameFeaturedUri to mostRecentGameInfo?.gameFeaturedImageUri?.toString(), + + PlayerColumns.playTogetherFriendStatus to relationshipInfo?.friendStatus, + PlayerColumns.playTogetherNickname to (relationshipInfo as? PlayerRelationshipInfoEntity)?.nickname, + PlayerColumns.playTogetherInvitationNickname to (relationshipInfo as? PlayerRelationshipInfoEntity)?.invitationNickname, + PlayerColumns.nicknameAbuseReportToken to (relationshipInfo as? PlayerRelationshipInfoEntity)?.nicknameAbuseReportToken, + + PlayerColumns.friendsListVisibility to currentPlayerInfo?.friendsListVisibilityStatus +) + +fun JSONObject.toPlayer() = PlayerEntity( + getString("playerId"), + getString("displayName"), + optString("avatarImageUrl").takeIf { it.isNotBlank() }?.toUri(), + optString("avatarImageUrl").takeIf { it.isNotBlank() }?.toUri(), + System.currentTimeMillis(), + 0, 0, + optString("avatarImageUrl").takeIf { it.isNotBlank() }, + optString("avatarImageUrl").takeIf { it.isNotBlank() }, + getString("title"), + null, + getJSONObject("experienceInfo")?.let { + PlayerLevelInfo( + it.optLong("currentExperiencePoints"), + 0, + it.getJSONObject("currentLevel")?.let { + PlayerLevel(it.getInt("level"), it.optLong("minExperiencePoints"), it.optLong("maxExperiencePoints")) + }, + it.getJSONObject("nextLevel")?.let { + PlayerLevel(it.getInt("level"), it.optLong("minExperiencePoints"), it.optLong("maxExperiencePoints")) + } + ) + }, + optJSONObject("profileSettings")?.optBoolean("profileVisible") ?: false, + false, + null, null, + optString("bannerUrlLandscape").takeIf { it.isNotBlank() }?.toUri(), + optString("bannerUrlLandscape").takeIf { it.isNotBlank() }, + optString("bannerUrlPortrait").takeIf { it.isNotBlank() }?.toUri(), + optString("bannerUrlPortrait").takeIf { it.isNotBlank() }, + 0, null, + optJSONObject("profileSettings")?.optString("friendsListVisibility")?.takeIf { it.isNotBlank() }?.let { + CurrentPlayerInfoEntity( + when (it) { + "VISIBLE" -> Player.FriendsListVisibilityStatus.VISIBLE + "REQUEST_REQUIRED" -> Player.FriendsListVisibilityStatus.REQUEST_REQUIRED + "FEATURE_UNAVAILABLE" -> Player.FriendsListVisibilityStatus.FEATURE_UNAVAILABLE + else -> Player.FriendsListVisibilityStatus.UNKNOWN + } + ) + }, + false, + null +) + +suspend fun registerForGames(context: Context, account: Account, queue: RequestQueue = Volley.newRequestQueue(context)) { + val authManager = AuthManager(context, account.name, Constants.GMS_PACKAGE_NAME, "oauth2:${Scopes.GAMES_FIRSTPARTY}") + authManager.setOauth2Foreground("1") + val authToken = withContext(Dispatchers.IO) { authManager.requestAuth(false).auth } + val androidId = getSettings(context, CheckIn.getContentUri(context), arrayOf(CheckIn.ANDROID_ID)) { cursor: Cursor -> cursor.getLong(0) } + val result = suspendCoroutine { 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 { + return mutableMapOf( + "Authorization" to "OAuth $authToken", + "X-Device-ID" to androidId.toString(16) + ) + } + } + ) + } + suspendCoroutine { 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 { + return mutableMapOf( + "Content-Type" to "application/json; charset=utf-8", + "Authorization" to "OAuth $authToken", + "X-Device-ID" to androidId.toString(16) + ) + } + } + ) + } +} + +suspend fun performGamesSignIn( + context: Context, + packageName: String, + account: Account, + permitted: Boolean = false, + scopes: List = emptyList(), + queue: RequestQueue = Volley.newRequestQueue(context) +): Boolean { + val scopes = (scopes.toSet() + Scope(Scopes.GAMES_LITE)).toList().sortedBy { it.scopeUri } + val authManager = AuthManager(context, account.name, packageName, "oauth2:${scopes.joinToString(" ")}") + if (scopes.size == 1) authManager.setItCaveatTypes("2") + if (permitted) authManager.isPermitted = true + val authResponse = withContext(Dispatchers.IO) { authManager.requestAuth(true) } + if (authResponse.auth == null) return false + if (authResponse.issueAdvice != "stored" || GamesConfigurationService.getPlayer(context, packageName, account) == null) { + suspend fun fetchSelfPlayer() = suspendCoroutine { continuation -> + queue.add( + object : JsonObjectRequest( + "https://www.googleapis.com/games/v1/players/me", + { continuation.resume(it) }, + { continuation.resumeWithException(it) }) { + override fun getHeaders(): MutableMap { + return mutableMapOf( + "Authorization" to "OAuth ${authResponse.auth}" + ) + } + } + ) + } + + val result = try { + fetchSelfPlayer() + } catch (e: Exception) { + if (e is VolleyError && e.networkResponse?.statusCode == 404) { + registerForGames(context, account, queue) + fetchSelfPlayer() + } else { + throw e + } + } + GamesConfigurationService.setPlayer(context, packageName, account, result.toString()) + if (packageName != GAMES_PACKAGE_NAME) { + try { + suspendCoroutine { continuation -> + queue.add(object : Request(Method.POST, "https://www.googleapis.com/games/v1/applications/played", { + continuation.resumeWithException(it) + }) { + override fun parseNetworkResponse(response: NetworkResponse): Response { + if (response.statusCode == 204) return success(Unit, null) + return Response.error(VolleyError(response)) + } + + override fun deliverResponse(response: Unit) { + continuation.resume(response) + } + + override fun getHeaders(): MutableMap { + return mutableMapOf( + "Authorization" to "OAuth ${authResponse.auth}" + ) + } + }) + } + } catch (ignored: Exception) { + } + } + } + return true +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/signin/SignInService.kt b/play-services-core/src/main/kotlin/org/microg/gms/signin/SignInService.kt index b8ecaf8a89..0880213bba 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/signin/SignInService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/signin/SignInService.kt @@ -6,11 +6,17 @@ package org.microg.gms.signin import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context import android.os.Bundle import android.os.Parcel import android.util.Log +import androidx.core.content.getSystemService +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.api.Scope import com.google.android.gms.common.internal.* import com.google.android.gms.signin.internal.* import org.microg.gms.BaseService @@ -24,12 +30,15 @@ class SignInService : BaseService(TAG, GmsService.SIGN_IN) { override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { val packageName = PackageUtils.getAndCheckCallingPackage(this, request.packageName) ?: throw IllegalArgumentException("Missing package name") - val binder = SignInServiceImpl().asBinder() + val binder = SignInServiceImpl(this, lifecycle, packageName, request.scopes).asBinder() callback.onPostInitComplete(CommonStatusCodes.SUCCESS, binder, Bundle()) } } -class SignInServiceImpl : ISignInService.Stub() { +class SignInServiceImpl(val context: Context, private val lifecycle: Lifecycle, val packageName: String, val scopes: Array) : ISignInService.Stub(), + LifecycleOwner { + override fun getLifecycle(): Lifecycle = lifecycle + override fun clearAccountFromSessionStore(sessionId: Int) { Log.d(TAG, "Not yet implemented: clearAccountFromSessionStore $sessionId") } @@ -51,13 +60,54 @@ class SignInServiceImpl : ISignInService.Stub() { } override fun signIn(request: SignInRequest?, callbacks: ISignInCallbacks?) { - Log.d(TAG, "Not yet implemented: signIn $request") - callbacks?.onSignIn(SignInResponse().apply { - connectionResult = ConnectionResult(ConnectionResult.INTERNAL_ERROR) - response = ResolveAccountResponse().apply { - connectionResult = ConnectionResult(ConnectionResult.INTERNAL_ERROR) - } - }) + Log.d(TAG, "signIn($request)") + val account = request?.request?.account + val result = if (account == null || context.getSystemService()?.getAccountsByType(account.type)?.contains(account) != true) + ConnectionResult(ConnectionResult.SIGN_IN_REQUIRED) else ConnectionResult(ConnectionResult.SUCCESS) + runCatching { + callbacks?.onSignIn(SignInResponse().apply { + connectionResult = result + response = ResolveAccountResponse().apply { + connectionResult = result + if (account != null) { + accountAccessor = object : IAccountAccessor.Stub() { + override fun getAccount(): Account { + return account + } + } + } + } + }) + } +// fun sendError() { +// runCatching { +// callbacks?.onSignIn(SignInResponse().apply { +// connectionResult = ConnectionResult(ConnectionResult.INTERNAL_ERROR) +// response = ResolveAccountResponse().apply { +// connectionResult = ConnectionResult(ConnectionResult.INTERNAL_ERROR) +// } +// }) +// } +// } +// Log.d(TAG, "Not yet implemented: signIn $request with $scopes") +// val account = request?.request?.account ?: return sendError() +// val authManager = AuthManager(context, account.name, packageName, "oauth2:${scopes.joinToString(" ") { it.scopeUri }}") +// authManager.setItCaveatTypes("2") +// if (!authManager.isPermitted && !AuthPrefs.isTrustGooglePermitted(context)) return sendError() +// lifecycleScope.launchWhenStarted { +// val authResponse = withContext(Dispatchers.IO) { +// authManager.requestAuth(true) +// } +// if (authResponse.auths == null) return@launchWhenStarted sendError() +// runCatching { +// callbacks?.onSignIn(SignInResponse().apply { +// connectionResult = ConnectionResult(ConnectionResult.SUCCESS) +// response = ResolveAccountResponse().apply { +// connectionResult = ConnectionResult(ConnectionResult.SUCCESS) +// } +// }) +// } +// } } override fun setGamesHasBeenGreeted(hasGreeted: Boolean) { @@ -84,5 +134,6 @@ class SignInServiceImpl : ISignInService.Stub() { Log.d(TAG, "Not yet implemented: resolveAccount") } - override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = + warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } } \ No newline at end of file diff --git a/play-services-games/build.gradle b/play-services-games/build.gradle new file mode 100644 index 0000000000..e1659236f2 --- /dev/null +++ b/play-services-games/build.gradle @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' +apply plugin: 'maven-publish' +apply plugin: 'signing' + +android { + namespace "com.google.android.gms.games" + + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + buildFeatures { + aidl = true + } + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + } + + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } +} + +apply from: '../gradle/publish-android.gradle' + +description = 'microG implementation of play-services-games' + +dependencies { + // Dependencies from play-services-games:23.1.0 + api project(':play-services-base') + api project(':play-services-basement') + api project(':play-services-drive') + api project(':play-services-tasks') + + annotationProcessor project(':safe-parcel-processor') +} diff --git a/play-services-games/src/main/AndroidManifest.xml b/play-services-games/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..115884d0b0 --- /dev/null +++ b/play-services-games/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + diff --git a/play-services-games/src/main/aidl/com/google/android/gms/games/PlayerEntity.aidl b/play-services-games/src/main/aidl/com/google/android/gms/games/PlayerEntity.aidl new file mode 100644 index 0000000000..b5670783fb --- /dev/null +++ b/play-services-games/src/main/aidl/com/google/android/gms/games/PlayerEntity.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.games; + +parcelable PlayerEntity; diff --git a/play-services-games/src/main/aidl/com/google/android/gms/games/client/IPlayGamesCallbacks.aidl b/play-services-games/src/main/aidl/com/google/android/gms/games/client/IPlayGamesCallbacks.aidl new file mode 100644 index 0000000000..649717be8d --- /dev/null +++ b/play-services-games/src/main/aidl/com/google/android/gms/games/client/IPlayGamesCallbacks.aidl @@ -0,0 +1,9 @@ +package com.google.android.gms.games.client; + +import com.google.android.gms.common.api.Status; +import com.google.android.gms.common.data.DataHolder; + +interface IPlayGamesCallbacks { + void onData(in DataHolder dataHolder) = 1000; + void onStatus5028(in Status status) = 5027; +} \ No newline at end of file diff --git a/play-services-games/src/main/aidl/com/google/android/gms/games/client/IPlayGamesService.aidl b/play-services-games/src/main/aidl/com/google/android/gms/games/client/IPlayGamesService.aidl new file mode 100644 index 0000000000..90331abbfe --- /dev/null +++ b/play-services-games/src/main/aidl/com/google/android/gms/games/client/IPlayGamesService.aidl @@ -0,0 +1,14 @@ +package com.google.android.gms.games.client; + +import com.google.android.gms.games.client.IPlayGamesCallbacks; +import com.google.android.gms.games.client.PlayGamesConsistencyTokens; + +interface IPlayGamesService { + void getGameCollection(IPlayGamesCallbacks callbacks, int maxResults, int gameCollectionType, boolean z, boolean forceReload) = 1000; + void loadGames(IPlayGamesCallbacks callbacks, String playerId, int maxResults, boolean z, boolean forceReload) = 1002; + + PlayGamesConsistencyTokens getConsistencyTokens() = 5027; + void updateConsistencyTokens(in PlayGamesConsistencyTokens tokens) = 5028; + + void fun5041(IPlayGamesCallbacks callbacks) = 5040; +} \ No newline at end of file diff --git a/play-services-games/src/main/aidl/com/google/android/gms/games/client/PlayGamesConsistencyTokens.aidl b/play-services-games/src/main/aidl/com/google/android/gms/games/client/PlayGamesConsistencyTokens.aidl new file mode 100644 index 0000000000..629ce58b62 --- /dev/null +++ b/play-services-games/src/main/aidl/com/google/android/gms/games/client/PlayGamesConsistencyTokens.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.games.client; + +parcelable PlayGamesConsistencyTokens; \ No newline at end of file diff --git a/play-services-games/src/main/aidl/com/google/android/gms/games/internal/IGamesCallbacks.aidl b/play-services-games/src/main/aidl/com/google/android/gms/games/internal/IGamesCallbacks.aidl new file mode 100644 index 0000000000..08b60e8352 --- /dev/null +++ b/play-services-games/src/main/aidl/com/google/android/gms/games/internal/IGamesCallbacks.aidl @@ -0,0 +1,49 @@ +package com.google.android.gms.games.internal; + +import com.google.android.gms.common.api.Status; +import com.google.android.gms.common.data.DataHolder; +import com.google.android.gms.games.multiplayer.realtime.RealTimeMessage; + +interface IGamesCallbacks { + /* @deprecated */ void onAuthTokenLoaded(int statusCode, String authToken) = 5000; + void onAchievementsLoaded(in DataHolder data) = 5001; + void onAchievementUpdated(int statusCode, String achievementId) = 5002; + void onLeaderboardsLoaded(in DataHolder data) = 5003; + void onLeaderboardScoresLoaded(in DataHolder leaderboard, in DataHolder scores) = 5004; + void onScoreSubmitted(in DataHolder data) = 5005; + void onPlayersLoaded(in DataHolder data) = 5006; + void onExtendedPlayersLoaded(in DataHolder data) = 5007; + /* @deprecated */ void onGamesLoaded(in DataHolder data) = 5008; + /* @deprecated */ void onExtendedGamesLoaded(in DataHolder data) = 5009; + /* @deprecated */ void onGameInstancesLoaded(in DataHolder data) = 5010; + /* @deprecated */ void onGameplayAclLoaded(in DataHolder data) = 5011; + /* @deprecated */ void onGameplayAclUpdated(int statusCode) = 5012; + /* @deprecated */ void onFAclLoaded(in DataHolder data) = 5013; + /* @deprecated */ void onFAclUpdated(int statusCode) = 5014; + void onSignOutComplete() = 5015; + /* @deprecated */ void onInvitationsLoaded(in DataHolder data) = 5016; + /* @deprecated */ void onRoomCreated(in DataHolder data) = 5017; + /* @deprecated */ void onJoinedRoom(in DataHolder data) = 5018; + /* @deprecated */ void onLeftRoom(int statusCode, String roomId) = 5019; + /* @deprecated */ void onRoomConnecting(in DataHolder data) = 5020; + /* @deprecated */ void onRoomAutoMatching(in DataHolder data) = 5021; + /* @deprecated */ void onRoomConnected(in DataHolder data) = 5022; + /* @deprecated */ void onConnectedToRoom(in DataHolder data) = 5023; + /* @deprecated */ void onDisconnectedFromRoom(in DataHolder data) = 5024; + /* @deprecated */ void onPeerInvitedToRoom(in DataHolder data, in String[] participantIds) = 5025; + /* @deprecated */ void onPeerJoinedRoom(in DataHolder data, in String[] participantIds) = 5026; + /* @deprecated */ void onPeerLeftRoom(in DataHolder data, in String[] participantIds) = 5027; + /* @deprecated */ void onPeerDeclined(in DataHolder data, in String[] participantIds) = 5028; + /* @deprecated */ void onPeerConnected(in DataHolder data, in String[] participantIds) = 5029; + /* @deprecated */ void onPeerDisconnected(in DataHolder data, in String[] participantIds) = 5030; + /* @deprecated */ void onRealTimeMessageReceived(in RealTimeMessage message) = 5031; + /* @deprecated */ void onMessageSent(int statusCode, int messageId, String recipientParticipantId) = 5032; + /* @deprecated */ void onGameMuteStatusChanged(int statusCode, String externalGameId, boolean isMuted) = 5033; + /* @deprecated */ void onNotifyAclLoaded(in DataHolder data) = 5034; + /* @deprecated */ void onNotifyAclUpdated(int statusCode) = 5035; + /* @deprecated */ void onInvitationReceived(in DataHolder data) = 5036; + /* @deprecated */ void onGameMuteStatusLoaded(in DataHolder data) = 5037; + /* @deprecated */ void onContactSettingsLoaded(in DataHolder data) = 5038; + /* @deprecated */ void onContactSettingsUpdated(int statusCode) = 5039; + void onServerAuthCode(in Status status, String serverAuthCode) = 25002; +} diff --git a/play-services-games/src/main/aidl/com/google/android/gms/games/internal/IGamesClient.aidl b/play-services-games/src/main/aidl/com/google/android/gms/games/internal/IGamesClient.aidl new file mode 100644 index 0000000000..24c61c3af8 --- /dev/null +++ b/play-services-games/src/main/aidl/com/google/android/gms/games/internal/IGamesClient.aidl @@ -0,0 +1,4 @@ +package com.google.android.gms.games.internal; + +interface IGamesClient { +} diff --git a/play-services-games/src/main/aidl/com/google/android/gms/games/internal/IGamesService.aidl b/play-services-games/src/main/aidl/com/google/android/gms/games/internal/IGamesService.aidl new file mode 100644 index 0000000000..010978f624 --- /dev/null +++ b/play-services-games/src/main/aidl/com/google/android/gms/games/internal/IGamesService.aidl @@ -0,0 +1,116 @@ +package com.google.android.gms.games.internal; + +import android.net.Uri; +import android.os.Bundle; +import android.os.IBinder; + +import com.google.android.gms.common.data.DataHolder; +//import com.google.android.gms.drive.Contents; +import com.google.android.gms.games.PlayerEntity; +import com.google.android.gms.games.internal.IGamesCallbacks; +import com.google.android.gms.games.internal.IGamesClient; + +interface IGamesService { + void clientDisconnecting(long clientId) = 5000; + void signOut(IGamesCallbacks callbacks) = 5001; + String getAppId() = 5002; + Bundle getConnectionHint() = 5003; + void showWelcomePopup(IBinder windowToken, in Bundle extras) = 5004; + void cancelPopups() = 5005; + String getCurrentAccountName() = 5006; + void loadGameplayAclInternal(IGamesCallbacks callbacks, String gameId) = 5007; + void updateGameplayAclInternal(IGamesCallbacks callbacks, String gameId, String aclData) = 5008; + void loadFAclInternal(IGamesCallbacks callbacks, String gameId) = 5009; + void updateFAclInternal(IGamesCallbacks callbacks, String gameId, boolean allCirclesVisible, in long[] circleIds) = 5010; + String getCurrentPlayerId() = 5011; + DataHolder getCurrentPlayer() = 5012; + void loadPlayer(IGamesCallbacks callbacks, String playerId) = 5013; + void loadInvitablePlayers(IGamesCallbacks callbacks, int pageSize, boolean expandCachedData, boolean forceReload) = 5014; + void submitScore(IGamesCallbacks callbacks, String leaderboardId, long score) = 5015; + void loadLeaderboards(IGamesCallbacks callbacks) = 5016; + void loadLeaderboard(IGamesCallbacks callbacks, String leaderboardId) = 5017; + void loadTopScores(IGamesCallbacks callbacks, String leaderboardId, int span, int leaderboardCollection, int maxResults, boolean forceReload) = 5018; + void loadPlayerCenteredScores(IGamesCallbacks callbacks, String leaderboardId, int span, int leaderboardCollection, int maxResults, boolean forceReload) = 5019; + void loadMoreScores(IGamesCallbacks callbacks, in Bundle previousheader, int maxResults, int pageDirection) = 5020; + void loadAchievements(IGamesCallbacks callbacks) = 5021; + void revealAchievement(IGamesCallbacks callbacks, String achievementId, IBinder windowToken, in Bundle extras) = 5022; + void unlockAchievement(IGamesCallbacks callbacks, String achievementId, IBinder windowToken, in Bundle extras) = 5023; + void incrementAchievement(IGamesCallbacks callbacks, String achievementId, int numSteps, IBinder windowToken, in Bundle extras) = 5024; + void loadGame(IGamesCallbacks callbacks) = 5025; + void loadInvitations(IGamesCallbacks callbacks) = 5026; + void declineInvitation(String invitationId, int invitationType) = 5027; + void dismissInvitation(String invitationId, int invitationType) = 5028; + void createRoom(IGamesCallbacks callbacks, IBinder processBinder, int variant, in String[] invitedPlayerIds, in Bundle autoMatchCriteria, boolean enableSockets, long clientId) = 5029; + void joinRoom(IGamesCallbacks callbacks, IBinder processBinder, String matchId, boolean enableSockets, long clientId) = 5030; + void leaveRoom(IGamesCallbacks callbacks, String matchId) = 5031; + int sendReliableMessage(IGamesCallbacks callbacks, in byte[] messageData, String matchId, String recipientParticipantId) = 5032; + int sendUnreliableMessage(in byte[] messageData, String matchId, in String[] recipientParticipantIds) = 5033; + String createSocketConnection(String participantId) = 5034; + void clearNotifications(int notificationTypes) = 5035; + void loadLeaderboardsFirstParty(IGamesCallbacks callbacks, String gameId) = 5036; + void loadLeaderboardFirstParty(IGamesCallbacks callbacks, String gameId, String leaderboardId) = 5037; + void loadTopScoresFirstParty(IGamesCallbacks callbacks, String gameId, String leaderboardId, int span, int leaderboardCollection, int maxResults, boolean forceReload) = 5038; + void loadPlayerCenteredScoresFirstParty(IGamesCallbacks callbacks, String gameId, String leaderboardId, int span, int leaderboardCollection, int maxResults, boolean forceReload) = 5039; + void loadAchievementsFirstParty(IGamesCallbacks callbacks, String playerId, String gameId) = 5040; + void loadGameFirstParty(IGamesCallbacks callbacks, String gameId) = 5041; + void loadGameInstancesFirstParty(IGamesCallbacks callbacks, String gameId) = 5042; + void loadGameCollectionFirstParty(IGamesCallbacks callbacks, int pageSize, int collectionType, boolean expandCachedData, boolean forceReload) = 5043; + void loadRecentlyPlayedGamesFirstParty(IGamesCallbacks callbacks, String externalPlayerId, int pageSize, boolean expandCachedData, boolean forceReload) = 5044; + void loadInvitablePlayersFirstParty(IGamesCallbacks callbacks, int pageSize, boolean expandCachedData, boolean forceReload) = 5045; + void loadRecentPlayersFirstParty(IGamesCallbacks callbacks) = 5046; + void loadCircledPlayersFirstParty(IGamesCallbacks callbacks, int pageSize, boolean expandCachedData, boolean forceReload) = 5047; + void loadSuggestedPlayersFirstParty(IGamesCallbacks callbacks) = 5048; + void dismissPlayerSuggestionFirstParty(String playerIdToDismiss) = 5049; + void declineInvitationFirstParty(String gameId, String invitationId, int invitationType) = 5050; + void loadInvitationsFirstParty(IGamesCallbacks callbacks, String gameId) = 5051; + int registerWaitingRoomListenerRestricted(IGamesCallbacks callbacks, String roomId) = 5052; + void setGameMuteStatusInternal(IGamesCallbacks callbacks, String gameId, boolean muted) = 5053; + void clearNotificationsFirstParty(String gameId, int notificationTypes) = 5054; + void loadNotifyAclInternal(IGamesCallbacks callbacks) = 5055; + void updateNotifyAclInternal(IGamesCallbacks callbacks, String aclData) = 5056; + void registerInvitationListener(IGamesCallbacks callbacks, long clientId) = 5057; + void unregisterInvitationListener(long clientId) = 5058; + int unregisterWaitingRoomListenerRestricted(String roomId) = 5059; + void isGameMutedInternal(IGamesCallbacks callbacks, String gameId) = 5060; + void loadContactSettingsInternal(IGamesCallbacks callbacks) = 5061; + void updateContactSettingsInternal(IGamesCallbacks callbacks, boolean enableMobileNotifications) = 5062; + String getSelectedAccountForGameFirstParty(String gamePackageName) = 5063; + void updateSelectedAccountForGameFirstParty(String gamePackageName, String accountName) = 5064; + Uri getGamesContentUriRestricted(String gameId) = 5065; + boolean shouldUseNewPlayerNotificationsFirstParty() = 5066; + void setUseNewPlayerNotificationsFirstParty(boolean newPlayerStyle) = 5067; + + void searchForPlayersFirstParty(IGamesCallbacks callbacks, String query, int pageSize, boolean expandCachedData, boolean forceReload) = 5500; + DataHolder getCurrentGame() = 5501; + + void loadAchievementsV2(IGamesCallbacks callbacks, boolean forceReload) = 6000; + + void submitLeaderboardScore(IGamesCallbacks callbacks, String leaderboardId, long score, @nullable String scoreTag) = 7001; + void setAchievementSteps(IGamesCallbacks callbacks, String id, int numSteps, IBinder windowToken, in Bundle extras) = 7002; + + Intent getAllLeaderboardsIntent() = 9002; + Intent getAchievementsIntent() = 9004; + Intent getPlayerSearchIntent() = 9009; + + void loadEvents(IGamesCallbacks callbacks, boolean forceReload) = 12015; + void incrementEvent(String eventId, int incrementAmount) = 12016; +// void discardAndCloseSnapshot(in Contents contents) = 12018; + void loadEventsById(IGamesCallbacks callbacks, boolean forceReload, in String[] eventsIds) = 12030; +// void resolveSnapshotConflict(IGamesCallbacks callbacks, String conflictId, String snapshotId, in SnapshotMetadataChangeEntity metadata, in Contents contents) = 12032; + int getMaxDataSize() = 12034; + int getMaxCoverImageSize() = 12035; + + void registerEventClient(IGamesClient callback, long l) = 15500; + Intent getCompareProfileIntentForPlayer(in PlayerEntity player) = 15502; + + void loadPlayerStats(IGamesCallbacks callbacks, boolean forceReload) = 17000; + + Account getCurrentAccount() = 21000; + + boolean isTelevision() = 22029; + + Intent getCompareProfileIntentWithAlternativeNameHints(String otherPlayerId, String otherPlayerInGameName, String currentPlayerInGameName) = 25015; + + void requestServerSideAccess(IGamesCallbacks callbacks, String serverClientId, boolean forceRefreshToken) = 27002; + +} diff --git a/play-services-games/src/main/aidl/com/google/android/gms/games/internal/connect/GamesSignInRequest.aidl b/play-services-games/src/main/aidl/com/google/android/gms/games/internal/connect/GamesSignInRequest.aidl new file mode 100644 index 0000000000..a3b015357d --- /dev/null +++ b/play-services-games/src/main/aidl/com/google/android/gms/games/internal/connect/GamesSignInRequest.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.games.internal.connect; + +parcelable GamesSignInRequest; \ No newline at end of file diff --git a/play-services-games/src/main/aidl/com/google/android/gms/games/internal/connect/GamesSignInResponse.aidl b/play-services-games/src/main/aidl/com/google/android/gms/games/internal/connect/GamesSignInResponse.aidl new file mode 100644 index 0000000000..e60e1b93f6 --- /dev/null +++ b/play-services-games/src/main/aidl/com/google/android/gms/games/internal/connect/GamesSignInResponse.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.games.internal.connect; + +parcelable GamesSignInResponse; \ No newline at end of file diff --git a/play-services-games/src/main/aidl/com/google/android/gms/games/internal/connect/IGamesConnectCallbacks.aidl b/play-services-games/src/main/aidl/com/google/android/gms/games/internal/connect/IGamesConnectCallbacks.aidl new file mode 100644 index 0000000000..4f2a16c1c1 --- /dev/null +++ b/play-services-games/src/main/aidl/com/google/android/gms/games/internal/connect/IGamesConnectCallbacks.aidl @@ -0,0 +1,8 @@ +package com.google.android.gms.games.internal.connect; + +import com.google.android.gms.common.api.Status; +import com.google.android.gms.games.internal.connect.GamesSignInResponse; + +interface IGamesConnectCallbacks { + void onSignIn(in Status status, in GamesSignInResponse response) = 1; +} \ No newline at end of file diff --git a/play-services-games/src/main/aidl/com/google/android/gms/games/internal/connect/IGamesConnectService.aidl b/play-services-games/src/main/aidl/com/google/android/gms/games/internal/connect/IGamesConnectService.aidl new file mode 100644 index 0000000000..3c8b37f35c --- /dev/null +++ b/play-services-games/src/main/aidl/com/google/android/gms/games/internal/connect/IGamesConnectService.aidl @@ -0,0 +1,8 @@ +package com.google.android.gms.games.internal.connect; + +import com.google.android.gms.games.internal.connect.GamesSignInRequest; +import com.google.android.gms.games.internal.connect.IGamesConnectCallbacks; + +interface IGamesConnectService { + void signIn(IGamesConnectCallbacks callback, in GamesSignInRequest request) = 1; +} \ No newline at end of file diff --git a/play-services-api/src/main/aidl/com/google/android/gms/games/multiplayer/realtime/RealTimeMessage.aidl b/play-services-games/src/main/aidl/com/google/android/gms/games/multiplayer/realtime/RealTimeMessage.aidl similarity index 100% rename from play-services-api/src/main/aidl/com/google/android/gms/games/multiplayer/realtime/RealTimeMessage.aidl rename to play-services-games/src/main/aidl/com/google/android/gms/games/multiplayer/realtime/RealTimeMessage.aidl diff --git a/play-services-games/src/main/java/com/google/android/gms/games/AnnotatedData.java b/play-services-games/src/main/java/com/google/android/gms/games/AnnotatedData.java new file mode 100644 index 0000000000..a39e04b3e7 --- /dev/null +++ b/play-services-games/src/main/java/com/google/android/gms/games/AnnotatedData.java @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.games; + +import org.microg.gms.common.Hide; + +/** + * Class to return annotated data. Currently, the only annotation is whether the data is stale or not. + */ +public class AnnotatedData { + private final T value; + private final boolean stale; + + @Hide + public AnnotatedData(T value, boolean stale) { + this.value = value; + this.stale = stale; + } + + /** + * Returns the data that is annotated by this class. + */ + public T get() { + return value; + } + + /** + * Returns {@code true} if the data returned by {@link #get()} is stale. This usually indicates that there was a network error and data was + * returned from the local cache. + */ + public boolean isStale() { + return stale; + } +} diff --git a/play-services-games/src/main/java/com/google/android/gms/games/CurrentPlayerInfo.java b/play-services-games/src/main/java/com/google/android/gms/games/CurrentPlayerInfo.java new file mode 100644 index 0000000000..1919568816 --- /dev/null +++ b/play-services-games/src/main/java/com/google/android/gms/games/CurrentPlayerInfo.java @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.games; + +import android.os.Parcelable; +import com.google.android.gms.common.data.Freezable; + +/** + * Data object representing the information related only to the signed in user. + */ +public interface CurrentPlayerInfo extends Freezable, Parcelable { + /** + * Retrieves if the user has shared the friends list with the game. The possible output can be found in {@link Player.FriendsListVisibilityStatus}. + */ + @Player.FriendsListVisibilityStatus + int getFriendsListVisibilityStatus(); +} diff --git a/play-services-games/src/main/java/com/google/android/gms/games/CurrentPlayerInfoEntity.java b/play-services-games/src/main/java/com/google/android/gms/games/CurrentPlayerInfoEntity.java new file mode 100644 index 0000000000..dfd7f6a85b --- /dev/null +++ b/play-services-games/src/main/java/com/google/android/gms/games/CurrentPlayerInfoEntity.java @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.games; + +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import org.microg.gms.common.Hide; +import org.microg.safeparcel.AutoSafeParcelable; + +@Hide +public class CurrentPlayerInfoEntity extends AutoSafeParcelable implements CurrentPlayerInfo { + @Field(1) + @Player.FriendsListVisibilityStatus + private int friendsListVisibilityStatus; + + public CurrentPlayerInfoEntity() { + } + + public CurrentPlayerInfoEntity(CurrentPlayerInfo copy) { + friendsListVisibilityStatus = copy.getFriendsListVisibilityStatus(); + } + + public CurrentPlayerInfoEntity(int friendsListVisibilityStatus) { + this.friendsListVisibilityStatus = friendsListVisibilityStatus; + } + + @Override + @Player.FriendsListVisibilityStatus + public int getFriendsListVisibilityStatus() { + return friendsListVisibilityStatus; + } + + @Override + public CurrentPlayerInfo freeze() { + return this; + } + + @Override + public boolean isDataValid() { + return true; + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(CurrentPlayerInfoEntity.class); +} diff --git a/play-services-games/src/main/java/com/google/android/gms/games/Player.java b/play-services-games/src/main/java/com/google/android/gms/games/Player.java new file mode 100644 index 0000000000..c9c1d1e86c --- /dev/null +++ b/play-services-games/src/main/java/com/google/android/gms/games/Player.java @@ -0,0 +1,197 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.games; + +import android.database.CharArrayBuffer; +import android.net.Uri; +import android.os.Parcelable; +import androidx.annotation.IntDef; +import com.google.android.gms.common.data.Freezable; +import com.google.android.gms.common.images.ImageManager; + +/** + * Data interface for retrieving player information. + */ +public interface Player extends Freezable, Parcelable { + /** + * Friends list visibility statuses. + */ + @IntDef({FriendsListVisibilityStatus.UNKNOWN, FriendsListVisibilityStatus.VISIBLE, FriendsListVisibilityStatus.REQUEST_REQUIRED, FriendsListVisibilityStatus.FEATURE_UNAVAILABLE}) + @interface FriendsListVisibilityStatus { + /** + * Constant indicating that currently it's unknown if the friends list is visible to the game, or whether the game can ask for + * access from the user. Use {@link PlayersClient#getCurrentPlayer(boolean)} to force reload the latest status. + */ + int UNKNOWN = 0; + /** + * Constant indicating that the friends list is currently visible to the game. + */ + int VISIBLE = 1; + /** + * Constant indicating that the friends list is not visible to the game, but the game can ask for access. + */ + int REQUEST_REQUIRED = 2; + /** + * Constant indicating that the friends list is currently unavailable for the game. You cannot request access at this time, + * either because the user has permanently declined or the friends feature is not available to them. In this state, any + * attempts to request access to the friends list will be unsuccessful. + */ + int FEATURE_UNAVAILABLE = 3; + } + + /** + * Player friend statuses. + */ + @IntDef({PlayerFriendStatus.UNKNOWN, PlayerFriendStatus.NO_RELATIONSHIP, PlayerFriendStatus.FRIEND}) + @interface PlayerFriendStatus { + /** + * Constant indicating that the currently signed-in player's friend status with this player is unknown. This may happen if the + * user has not shared the friends list with the game. + */ + int UNKNOWN = -1; + /** + * Constant indicating that the currently signed-in player is not a friend of this player, and there are no pending invitations + * between them. + */ + int NO_RELATIONSHIP = 0; + /** + * Constant indicating that the currently signed-in player and this player are friends. + */ + int FRIEND = 4; + } + + /** + * Constant indicating that the current XP total for a player is not known. + */ + long CURRENT_XP_UNKNOWN = -1; + /** + * Constant indicating that a timestamp for a player is not known. + */ + long TIMESTAMP_UNKNOWN = -1; + + /** + * Retrieves the URI for loading this player's landscape banner image. Returns null if the player has no landscape banner image. + *

+ * To retrieve the Image from the {@link Uri}, use {@link ImageManager}. + * + * @return The image URI for the player's landscape banner image, or null if the player has none. + */ + Uri getBannerImageLandscapeUri(); + + /** + * Retrieves the URI for loading this player's portrait banner image. Returns null if the player has no portrait banner image. + *

+ * To retrieve the Image from the {@link Uri}, use {@link ImageManager}. + * + * @return The image URI for the player's portrait banner image, or null if the player has none. + */ + Uri getBannerImagePortraitUri(); + + /** + * Returns information only available for the signed-in user. The method will return {@code null} for other players. + */ + CurrentPlayerInfo getCurrentPlayerInfo(); + + /** + * Loads the player's display name into the given {@link CharArrayBuffer}. + * + * @param dataOut The buffer to load the data into. + */ + void getDisplayName(CharArrayBuffer dataOut); + + /** + * Retrieves the display name for this player. + * + * @return The player's display name. + */ + String getDisplayName(); + + /** + * Retrieves the URI for loading this player's hi-res profile image. Returns null if the player has no profile image. + *

+ * To retrieve the Image from the {@link Uri}, use {@link ImageManager}. + * + * @return The image URI for the player's hi-res profile image, or null if the player has none. + */ + Uri getHiResImageUri(); + + /** + * Retrieves the URI for loading this player's icon-size profile image. Returns null if the player has no profile image. + *

+ * To retrieve the Image from the {@link Uri}, use {@link ImageManager}. + * + * @return The image URI for the player's icon-size profile image, or null if the player has none. + */ + Uri getIconImageUri(); + + /** + * Retrieves the timestamp at which this player last played a multiplayer game with the currently signed in user. If the + * timestamp is not found, this method returns {@link #TIMESTAMP_UNKNOWN}. + * + * @return The timestamp (in ms since epoch) at which the player last played a multiplayer game with the currently signed in user. + * @deprecated Real-time multiplayer and Turn-based multiplayer support is being shut down on March 31, 2020. + */ + @Deprecated + long getLastPlayedWithTimestamp(); + + /** + * Retrieves the player level associated information if any exists. If no level information exists for this player, this method will return {@code null}. + * + * @return The {@link PlayerLevelInfo} associated with this player, if any. + */ + PlayerLevelInfo getLevelInfo(); + + /** + * Retrieves the ID of this player. + * + * @return The player ID. + */ + String getPlayerId(); + + /** + * Returns relationship information of this player. If no relationship information exists for this player, this method will return {@code null}. + */ + PlayerRelationshipInfo getRelationshipInfo(); + + /** + * Retrieves the timestamp at which this player record was last updated locally. + * + * @return The timestamp (in ms since epoch) at which the player data was last updated locally. + */ + long getRetrievedTimestamp(); + + /** + * Retrieves the title of the player. This is based on the player's gameplay activity in apps using Google Play Games + * services. Note that not all players have titles, and that a player's title may change over time. + * + * @return The player's title, or {@code null} if this player has no title. + */ + String getTitle(); + + /** + * Loads the player's title into the given {@link CharArrayBuffer}. + * + * @param dataOut The buffer to load the data into. + */ + void getTitle(CharArrayBuffer dataOut); + + /** + * Indicates whether this player has a hi-res profile image to display. + * + * @return Whether the player has a hi-res profile image to display. + */ + boolean hasHiResImage(); + + /** + * Indicates whether this player has an icon-size profile image to display. + * + * @return Whether the player has an icon-size profile image to display. + */ + boolean hasIconImage(); +} diff --git a/play-services-games/src/main/java/com/google/android/gms/games/PlayerBuffer.java b/play-services-games/src/main/java/com/google/android/gms/games/PlayerBuffer.java new file mode 100644 index 0000000000..3295e6944e --- /dev/null +++ b/play-services-games/src/main/java/com/google/android/gms/games/PlayerBuffer.java @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.games; + +import com.google.android.gms.common.data.AbstractDataBuffer; +import com.google.android.gms.common.data.DataHolder; +import org.microg.gms.common.Hide; + +/** + * Data structure providing access to a list of players. + */ +public class PlayerBuffer extends AbstractDataBuffer { + @Hide + public PlayerBuffer(DataHolder dataHolder) { + super(dataHolder); + } + + public Player get(int position) { + throw new UnsupportedOperationException(); + //return new PlayerRef(dataHolder, position); + } +} diff --git a/play-services-games/src/main/java/com/google/android/gms/games/PlayerColumns.java b/play-services-games/src/main/java/com/google/android/gms/games/PlayerColumns.java new file mode 100644 index 0000000000..6d08957bab --- /dev/null +++ b/play-services-games/src/main/java/com/google/android/gms/games/PlayerColumns.java @@ -0,0 +1,80 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.games; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class PlayerColumns { + public static final String externalPlayerId = "external_player_id"; + public static final String profileName = "profile_name"; + public static final String profileIconImageId = "profile_icon_image_id"; + public static final String profileIconImageUri = "profile_icon_image_uri"; + public static final String profileIconImageUrl = "profile_icon_image_url"; + public static final String profileHiResImageId = "profile_hi_res_image_id"; + public static final String profileHiResImageUri = "profile_hi_res_image_uri"; + public static final String profileHiResImageUrl = "profile_hi_res_image_url"; + public static final String lastUpdated = "last_updated"; + public static final String isInCircles = "is_in_circles"; + public static final String playedWithTimestamp = "played_with_timestamp"; + public static final String currentXpTotal = "current_xp_total"; + public static final String currentLevel = "current_level"; + public static final String currentLevelMinXp = "current_level_min_xp"; + public static final String currentLevelMaxXp = "current_level_max_xp"; + public static final String nextLevel = "next_level"; + public static final String nextLevelMaxXp = "next_level_max_xp"; + public static final String lastLevelUpTimestamp = "last_level_up_timestamp"; + public static final String playerTitle = "player_title"; + public static final String hasAllPublicAcls = "has_all_public_acls"; + public static final String isProfileVisible = "is_profile_visible"; + public static final String mostRecentExternalGameId = "most_recent_external_game_id"; + public static final String mostRecentGameName = "most_recent_game_name"; + public static final String mostRecentActivityTimestamp = "most_recent_activity_timestamp"; + public static final String mostRecentGameIconId = "most_recent_game_icon_id"; + public static final String mostRecentGameIconUri = "most_recent_game_icon_uri"; + public static final String mostRecentGameHiResId = "most_recent_game_hi_res_id"; + public static final String mostRecentGameHiResUri = "most_recent_game_hi_res_uri"; + public static final String mostRecentGameFeaturedId = "most_recent_game_featured_id"; + public static final String mostRecentGameFeaturedUri = "most_recent_game_featured_uri"; + public static final String hasDebugAccess = "has_debug_access"; + public static final String gamerTag = "gamer_tag"; + public static final String realName = "real_name"; + public static final String bannerImageLandscapeId = "banner_image_landscape_id"; + public static final String bannerImageLandscapeUri = "banner_image_landscape_uri"; + public static final String bannerImageLandscapeUrl = "banner_image_landscape_url"; + public static final String bannerImagePortraitId = "banner_image_portrait_id"; + public static final String bannerImagePortraitUri = "banner_image_portrait_uri"; + public static final String bannerImagePortraitUrl = "banner_image_portrait_url"; + public static final String gamerFriendStatus = "gamer_friend_status"; + public static final String gamerFriendUpdateTimestamp = "gamer_friend_update_timestamp"; + public static final String isMuted = "is_muted"; + public static final String totalUnlockedAchievements = "total_unlocked_achievements"; + public static final String playTogetherFriendStatus = "play_together_friend_status"; + public static final String playTogetherNickname = "play_together_nickname"; + public static final String playTogetherInvitationNickname = "play_together_invitation_nickname"; + public static final String nicknameAbuseReportToken = "nickname_abuse_report_token"; + public static final String friendsListVisibility = "friends_list_visibility"; + public static final String alwaysAutoSignIn = "always_auto_sign_in"; + public static final String profileCreationTimestamp = "profile_creation_timestamp"; + public static final String gamePlayerId = "game_player_id"; + + public static final List CURRENT_PLAYER_COLUMNS = Collections.unmodifiableList(Arrays.asList( + externalPlayerId, + profileIconImageId, profileHiResImageId, profileIconImageUri, profileIconImageUrl, profileHiResImageUri, profileHiResImageUrl, + profileName, lastUpdated, isInCircles, hasAllPublicAcls, hasDebugAccess, isProfileVisible, + currentXpTotal, currentLevel, currentLevelMinXp, currentLevelMaxXp, nextLevel, nextLevelMaxXp, lastLevelUpTimestamp, + playerTitle, + mostRecentExternalGameId, mostRecentGameName, mostRecentActivityTimestamp, mostRecentGameIconId, mostRecentGameIconUri, mostRecentGameHiResId, mostRecentGameHiResUri, mostRecentGameFeaturedId, mostRecentGameFeaturedUri, + gamerTag, realName, + bannerImageLandscapeId, bannerImageLandscapeUri, bannerImageLandscapeUrl, bannerImagePortraitId, bannerImagePortraitUri, bannerImagePortraitUrl, + totalUnlockedAchievements, + playTogetherFriendStatus, playTogetherNickname, playTogetherInvitationNickname, + profileCreationTimestamp, nicknameAbuseReportToken, friendsListVisibility, alwaysAutoSignIn, + gamerFriendStatus, gamerFriendUpdateTimestamp, + isMuted, gamePlayerId + )); +} diff --git a/play-services-games/src/main/java/com/google/android/gms/games/PlayerEntity.java b/play-services-games/src/main/java/com/google/android/gms/games/PlayerEntity.java new file mode 100644 index 0000000000..64204d4b03 --- /dev/null +++ b/play-services-games/src/main/java/com/google/android/gms/games/PlayerEntity.java @@ -0,0 +1,276 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.games; + +import android.database.CharArrayBuffer; +import android.net.Uri; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import com.google.android.gms.games.internal.player.MostRecentGameInfoEntity; +import org.microg.gms.common.Hide; +import org.microg.safeparcel.AutoSafeParcelable; + +/** + * Data object representing a set of Player data. This is immutable, and therefore safe to cache or store. Note, however, that + * the data it represents may grow stale. + *

+ * This class exists solely to support parceling these objects and should not be used directly. + */ +public class PlayerEntity extends AutoSafeParcelable implements Player { + @Field(1) + private String playerId; + @Field(2) + private String displayName; + @Field(3) + private Uri iconImageUri; + @Field(4) + private Uri hiResImageUri; + @Field(5) + private long retrievedTimestamp; + @Field(6) + private int isInCircles; + @Field(7) + private long lastPlayedWithTimestamp; + @Field(8) + private String iconImageUrl; + @Field(9) + private String hiResImageUrl; + @Field(14) + private String title; + @Field(15) + private MostRecentGameInfoEntity mostRecentGameInfo; + @Field(16) + private PlayerLevelInfo levelInfo; + @Field(18) + private boolean profileVisible; + @Field(19) + private boolean hasDebugAccess; + @Field(20) + private String gamerTag; + @Field(21) + private String name; + @Field(22) + private Uri bannerImageLandscapeUri; + @Field(23) + private String bannerImageLandscapeUrl; + @Field(24) + private Uri bannerImagePortraitUri; + @Field(25) + private String bannerImagePortraitUrl; + @Field(29) + private long totalUnlockedAchievement = -1; + @Field(33) + private PlayerRelationshipInfoEntity relationshipInfo; + @Field(35) + private CurrentPlayerInfoEntity currentPlayerInfo; + @Field(36) + private boolean alwaysAutoSignIn; + @Field(37) + private String gamePlayerId; + + @Hide + public PlayerEntity() { + } + + @Hide + public PlayerEntity(Player copy) { + bannerImageLandscapeUri = copy.getBannerImageLandscapeUri(); + bannerImagePortraitUri = copy.getBannerImagePortraitUri(); + currentPlayerInfo = new CurrentPlayerInfoEntity(copy.getCurrentPlayerInfo()); + displayName = copy.getDisplayName(); + hiResImageUri = copy.getHiResImageUri(); + iconImageUri = copy.getIconImageUri(); + lastPlayedWithTimestamp = copy.getLastPlayedWithTimestamp(); + levelInfo = copy.getLevelInfo(); + playerId = copy.getPlayerId(); + relationshipInfo = new PlayerRelationshipInfoEntity(copy.getRelationshipInfo()); + retrievedTimestamp = copy.getRetrievedTimestamp(); + title = copy.getTitle(); + } + + @Hide + public PlayerEntity(String playerId, String displayName, Uri iconImageUri, Uri hiResImageUri, long retrievedTimestamp, int isInCircles, long lastPlayedWithTimestamp, String iconImageUrl, String hiResImageUrl, String title, MostRecentGameInfoEntity mostRecentGameInfo, PlayerLevelInfo levelInfo, boolean profileVisible, boolean hasDebugAccess, String gamerTag, String name, Uri bannerImageLandscapeUri, String bannerImageLandscapeUrl, Uri bannerImagePortraitUri, String bannerImagePortraitUrl, long totalUnlockedAchievement, PlayerRelationshipInfoEntity relationshipInfo, CurrentPlayerInfoEntity currentPlayerInfo, boolean alwaysAutoSignIn, String gamePlayerId) { + this.playerId = playerId; + this.displayName = displayName; + this.iconImageUri = iconImageUri; + this.hiResImageUri = hiResImageUri; + this.retrievedTimestamp = retrievedTimestamp; + this.isInCircles = isInCircles; + this.lastPlayedWithTimestamp = lastPlayedWithTimestamp; + this.iconImageUrl = iconImageUrl; + this.hiResImageUrl = hiResImageUrl; + this.title = title; + this.mostRecentGameInfo = mostRecentGameInfo; + this.levelInfo = levelInfo; + this.profileVisible = profileVisible; + this.hasDebugAccess = hasDebugAccess; + this.gamerTag = gamerTag; + this.name = name; + this.bannerImageLandscapeUri = bannerImageLandscapeUri; + this.bannerImageLandscapeUrl = bannerImageLandscapeUrl; + this.bannerImagePortraitUri = bannerImagePortraitUri; + this.bannerImagePortraitUrl = bannerImagePortraitUrl; + this.totalUnlockedAchievement = totalUnlockedAchievement; + this.relationshipInfo = relationshipInfo; + this.currentPlayerInfo = currentPlayerInfo; + this.alwaysAutoSignIn = alwaysAutoSignIn; + this.gamePlayerId = gamePlayerId; + } + + @Override + public Uri getBannerImageLandscapeUri() { + return bannerImageLandscapeUri; + } + + @Override + public Uri getBannerImagePortraitUri() { + return bannerImagePortraitUri; + } + + @Override + public CurrentPlayerInfo getCurrentPlayerInfo() { + return currentPlayerInfo; + } + + @Override + public void getDisplayName(CharArrayBuffer dataOut) { + throw new UnsupportedOperationException(); + } + + @Override + public String getDisplayName() { + return displayName; + } + + @Override + public Uri getHiResImageUri() { + return hiResImageUri; + } + + @Override + public Uri getIconImageUri() { + return iconImageUri; + } + + @Override + public long getLastPlayedWithTimestamp() { + return lastPlayedWithTimestamp; + } + + @Override + public PlayerLevelInfo getLevelInfo() { + return levelInfo; + } + + @Override + public String getPlayerId() { + return playerId; + } + + @Override + public PlayerRelationshipInfo getRelationshipInfo() { + return relationshipInfo; + } + + @Override + public long getRetrievedTimestamp() { + return retrievedTimestamp; + } + + @Override + public String getTitle() { + return title; + } + + @Override + public void getTitle(CharArrayBuffer dataOut) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasHiResImage() { + return hiResImageUri != null; + } + + @Override + public boolean hasIconImage() { + return iconImageUri != null; + } + + @Override + public boolean isDataValid() { + return true; + } + + @Override + public Player freeze() { + return this; + } + + @Hide + public String getGamerTag() { + return gamerTag; + } + + @Hide + public String getName() { + return name; + } + + @Hide + public String getIconImageUrl() { + return iconImageUrl; + } + + @Hide + public String getHiResImageUrl() { + return hiResImageUrl; + } + + @Hide + public String getBannerImageLandscapeUrl() { + return bannerImageLandscapeUrl; + } + + @Hide + public String getBannerImagePortraitUrl() { + return bannerImagePortraitUrl; + } + + @Hide + public int getIsInCircles() { + return isInCircles; + } + + @Hide + public boolean isProfileVisible() { + return profileVisible; + } + + @Hide + public boolean getHasDebugAccess() { + return hasDebugAccess; + } + + @Hide + public long getTotalUnlockedAchievement() { + return totalUnlockedAchievement; + } + + @Hide + public boolean isAlwaysAutoSignIn() { + return alwaysAutoSignIn; + } + + @Hide + public MostRecentGameInfoEntity getMostRecentGameInfo() { + return mostRecentGameInfo; + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(PlayerEntity.class); +} diff --git a/play-services-games/src/main/java/com/google/android/gms/games/PlayerLevel.java b/play-services-games/src/main/java/com/google/android/gms/games/PlayerLevel.java new file mode 100644 index 0000000000..a56d0d2ee5 --- /dev/null +++ b/play-services-games/src/main/java/com/google/android/gms/games/PlayerLevel.java @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.games; + +import android.os.Parcel; +import androidx.annotation.NonNull; +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import org.microg.gms.common.Hide; + +/** + * Data object representing a level a player can obtain in the metagame. + *

+ * A {@code PlayerLevel} has three components: a numeric value, and a range of XP totals it represents. A player is considered a + * given level if they have at least {@link #getMinXp()} and less than {@link #getMaxXp()}. + */ +@SafeParcelable.Class +public class PlayerLevel extends AbstractSafeParcelable { + @Field(1) + private final int levelNumber; + @Field(2) + private final long minXp; + @Field(3) + private final long maxXp; + + @Constructor + @Hide + public PlayerLevel(@Param(1) int levelNumber, @Param(2) long minXp, @Param(3) long maxXp) { + this.levelNumber = levelNumber; + this.minXp = minXp; + this.maxXp = maxXp; + } + + /** + * Returns the number for this level, e.g. "level 10". + *

+ * This is the level that this object represents. For a player to be considered as being of this level, the value given by + * {@link PlayerLevelInfo#getCurrentXpTotal()} must fall in the range [{@link #getMinXp()}, {@link #getMaxXp()}). + * + * @return The level number for this level. + */ + public int getLevelNumber() { + return levelNumber; + } + + /** + * @return The maximum XP value represented by this level, exclusive. + */ + public long getMaxXp() { + return maxXp; + } + + /** + * @return The minimum XP value needed to attain this level, inclusive. + */ + public long getMinXp() { + return minXp; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(PlayerLevel.class); +} diff --git a/play-services-games/src/main/java/com/google/android/gms/games/PlayerLevelInfo.java b/play-services-games/src/main/java/com/google/android/gms/games/PlayerLevelInfo.java new file mode 100644 index 0000000000..f1c0dc794b --- /dev/null +++ b/play-services-games/src/main/java/com/google/android/gms/games/PlayerLevelInfo.java @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.games; + +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import org.microg.gms.common.Hide; +import org.microg.safeparcel.AutoSafeParcelable; + +/** + * Data object representing the current level information of a player in the metagame. + *

+ * A {@code PlayerLevelInfo} has four components: the player's current XP, the timestamp of the player's last level-up, the + * player's current level, and the player's next level. + */ +public class PlayerLevelInfo extends AutoSafeParcelable { + @Field(1) + private long currentXpTotal; + @Field(2) + private long lastLevelUpTimestamp; + @Field(3) + private PlayerLevel currentLevel; + @Field(4) + private PlayerLevel nextLevel; + + @Hide + public PlayerLevelInfo() { + } + + @Hide + public PlayerLevelInfo(long currentXpTotal, long lastLevelUpTimestamp, PlayerLevel currentLevel, PlayerLevel nextLevel) { + this.currentXpTotal = currentXpTotal; + this.lastLevelUpTimestamp = lastLevelUpTimestamp; + this.currentLevel = currentLevel; + this.nextLevel = nextLevel; + } + + /** + * Getter for the player's current level object. This object will be the same as the one returned from {@link #getNextLevel()} if the + * player reached the maximum level. + * + * @return The player's current level object. + * @see #isMaxLevel() + */ + public PlayerLevel getCurrentLevel() { + return currentLevel; + } + + /** + * @return The player's current XP value. + */ + public long getCurrentXpTotal() { + return currentXpTotal; + } + + /** + * @return The timestamp of the player's last level-up. + */ + public long getLastLevelUpTimestamp() { + return lastLevelUpTimestamp; + } + + /** + * Getter for the player's next level object. This object will be the same as the one returned from {@link #getCurrentLevel()} if the + * player reached the maximum level. + * + * @return The player's next level object. + * @see #isMaxLevel() + */ + public PlayerLevel getNextLevel() { + return nextLevel; + } + + /** + * @return {@code true} if the player reached the maximum level ({@link #getCurrentLevel()} is the same as {@link #getNextLevel()}. + */ + public boolean isMaxLevel() { + return currentLevel.equals(nextLevel); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(PlayerLevelInfo.class); +} diff --git a/play-services-games/src/main/java/com/google/android/gms/games/PlayerRef.java b/play-services-games/src/main/java/com/google/android/gms/games/PlayerRef.java new file mode 100644 index 0000000000..ee6d899534 --- /dev/null +++ b/play-services-games/src/main/java/com/google/android/gms/games/PlayerRef.java @@ -0,0 +1,133 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.games; + +import android.database.CharArrayBuffer; +import android.net.Uri; +import android.os.Parcel; +import androidx.annotation.NonNull; +import com.google.android.gms.common.data.DataBufferRef; +import com.google.android.gms.common.data.DataHolder; +import org.microg.gms.common.Hide; + +@Hide +public class PlayerRef extends DataBufferRef implements Player { + public PlayerRef(DataHolder dataHolder, int position) { + super(dataHolder, position); + } + + private Uri toUri(String str) { + if (str == null) return null; + return Uri.parse(str); + } + + @Override + public Uri getBannerImageLandscapeUri() { + return toUri(getString(PlayerColumns.bannerImageLandscapeUri)); + } + + @Override + public Uri getBannerImagePortraitUri() { + return toUri(getString(PlayerColumns.bannerImagePortraitUri)); + } + + @Override + public CurrentPlayerInfo getCurrentPlayerInfo() { + return null; + } + + @Override + public void getDisplayName(CharArrayBuffer dataOut) { + copyToBuffer(PlayerColumns.profileName, dataOut); + } + + @Override + public String getDisplayName() { + return getString(PlayerColumns.profileName); + } + + @Override + public Uri getHiResImageUri() { + return toUri(getString(PlayerColumns.profileHiResImageUri)); + } + + @Override + public Uri getIconImageUri() { + return toUri(getString(PlayerColumns.profileIconImageUri)); + } + + @Override + public long getLastPlayedWithTimestamp() { + return getLong(PlayerColumns.playedWithTimestamp); + } + + @Override + public PlayerLevelInfo getLevelInfo() { + return null; + } + + @Override + public String getPlayerId() { + return getString(PlayerColumns.externalPlayerId); + } + + @Override + public PlayerRelationshipInfo getRelationshipInfo() { + return null; + } + + @Override + public long getRetrievedTimestamp() { + return getLong(PlayerColumns.lastUpdated); + } + + @Override + public String getTitle() { + return getString(PlayerColumns.playerTitle); + } + + @Override + public void getTitle(CharArrayBuffer dataOut) { + copyToBuffer(PlayerColumns.playerTitle, dataOut); + } + + @Override + public boolean hasHiResImage() { + return hasColumn(PlayerColumns.profileHiResImageUri); + } + + @Override + public boolean hasIconImage() { + return hasColumn(PlayerColumns.profileIconImageUri); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + new PlayerEntity(this).writeToParcel(dest, flags); + } + + @Override + public Player freeze() { + return null; + } + + public static final Creator CREATOR = new Creator() { + @Override + public Player createFromParcel(Parcel source) { + return PlayerEntity.CREATOR.createFromParcel(source); + } + + @Override + public Player[] newArray(int size) { + return new Player[size]; + } + }; +} diff --git a/play-services-games/src/main/java/com/google/android/gms/games/PlayerRelationshipInfo.java b/play-services-games/src/main/java/com/google/android/gms/games/PlayerRelationshipInfo.java new file mode 100644 index 0000000000..edac385376 --- /dev/null +++ b/play-services-games/src/main/java/com/google/android/gms/games/PlayerRelationshipInfo.java @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.games; + +import android.os.Parcelable; +import com.google.android.gms.common.data.Freezable; + +/** + * Data object representing the relationship information of a player. + */ +public interface PlayerRelationshipInfo extends Freezable, Parcelable { + /** + * Retrieves this player's friend status relative to the currently signed-in player. The possible output can be found in + * {@link Player.PlayerFriendStatus}. + */ + @Player.PlayerFriendStatus + int getFriendStatus(); +} diff --git a/play-services-games/src/main/java/com/google/android/gms/games/PlayerRelationshipInfoEntity.java b/play-services-games/src/main/java/com/google/android/gms/games/PlayerRelationshipInfoEntity.java new file mode 100644 index 0000000000..e34e328248 --- /dev/null +++ b/play-services-games/src/main/java/com/google/android/gms/games/PlayerRelationshipInfoEntity.java @@ -0,0 +1,67 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.games; + +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import org.microg.gms.common.Hide; +import org.microg.safeparcel.AutoSafeParcelable; + +@Hide +public class PlayerRelationshipInfoEntity extends AutoSafeParcelable implements PlayerRelationshipInfo { + @Field(1) + @Player.PlayerFriendStatus + private int friendStatus; + @Field(2) + private String nickname; + @Field(3) + private String invitationNickname; + @Field(4) + private String nicknameAbuseReportToken; + + public PlayerRelationshipInfoEntity() { + } + + public PlayerRelationshipInfoEntity(PlayerRelationshipInfo copy) { + friendStatus = copy.getFriendStatus(); + } + + public PlayerRelationshipInfoEntity(int friendStatus, String nickname, String invitationNickname, String nicknameAbuseReportToken) { + this.friendStatus = friendStatus; + this.nickname = nickname; + this.invitationNickname = invitationNickname; + this.nicknameAbuseReportToken = nicknameAbuseReportToken; + } + + @Override + @Player.PlayerFriendStatus + public int getFriendStatus() { + return friendStatus; + } + + @Override + public PlayerRelationshipInfo freeze() { + return this; + } + + @Override + public boolean isDataValid() { + return true; + } + + public String getNickname() { + return nickname; + } + + public String getInvitationNickname() { + return invitationNickname; + } + + public String getNicknameAbuseReportToken() { + return nicknameAbuseReportToken; + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(PlayerRelationshipInfoEntity.class); +} diff --git a/play-services-games/src/main/java/com/google/android/gms/games/Players.java b/play-services-games/src/main/java/com/google/android/gms/games/Players.java new file mode 100644 index 0000000000..4172ce0ac4 --- /dev/null +++ b/play-services-games/src/main/java/com/google/android/gms/games/Players.java @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.games; + +/** + * Entry point for player functionality. + * + * @deprecated Use {@link PlayersClient} instead + */ +@Deprecated +public interface Players { +} diff --git a/play-services-games/src/main/java/com/google/android/gms/games/PlayersClient.java b/play-services-games/src/main/java/com/google/android/gms/games/PlayersClient.java new file mode 100644 index 0000000000..c61e4e3fda --- /dev/null +++ b/play-services-games/src/main/java/com/google/android/gms/games/PlayersClient.java @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.games; + +import android.app.Activity; +import android.content.Intent; +import android.os.RemoteException; +import com.google.android.gms.tasks.Task; + +/** + * A client to interact with Players. + */ +public interface PlayersClient { + /** + * Used by the Player Search UI to return a list of parceled Player objects. Retrieve with {@link Intent#getParcelableArrayListExtra(String)}. + * + * @see #getPlayerSearchIntent() + */ + String EXTRA_PLAYER_SEARCH_RESULTS = "player_search_results"; + + /** + * Returns a {@link Task} which asynchronously loads the current signed-in {@link Player}, if available. + *

+ * The returned {@code Task} can fail with a {@link RemoteException}. + * + * @param forceReload If {@code true}, this call will clear any locally-cached data and attempt to fetch the latest data from the server. This would + * commonly be used for something like a user-initiated refresh. Normally, this should be set to {@code false} to gain advantages + * of data caching. + */ + Task> getCurrentPlayer(boolean forceReload); + + /** + * Returns a {@link Task} which asynchronously loads the current signed-in {@link Player}, if available. + *

+ * The returned {@code Task} can fail with a {@link RemoteException}. + */ + Task getCurrentPlayer(); + + /** + * Returns a {@link Task} which asynchronously loads the current signed-in player ID, if available. + *

+ * The returned {@code Task} can fail with a {@link RemoteException}. + */ + Task getCurrentPlayerId(); + + /** + * Returns a {@link Task} which asynchronously loads an {@link Intent} that will display a screen where the user can search for players. + *

+ * Note that this must be invoked with {@link Activity#startActivityForResult(Intent, int)}, so that the identity of the + * calling package can be established. + *

+ * If the user canceled, the result will be {@link Activity#RESULT_CANCELED}. If the user selected any players from the search + * results list, the result will be {@link Activity#RESULT_OK}, and the data intent will contain a list of parceled Player objects in + * {@link #EXTRA_PLAYER_SEARCH_RESULTS}. + *

+ * Note that the current Player Search UI only allows a single selection, so the returned list of parceled Player objects will + * currently contain at most one Player. + *

+ * The returned {@code Task} can fail with a {@link RemoteException}. + */ + Task getPlayerSearchIntent(); +} diff --git a/play-services-games/src/main/java/com/google/android/gms/games/achievement/package-info.java b/play-services-games/src/main/java/com/google/android/gms/games/achievement/package-info.java new file mode 100644 index 0000000000..e6a223f2f4 --- /dev/null +++ b/play-services-games/src/main/java/com/google/android/gms/games/achievement/package-info.java @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: CC-BY-4.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ +/** + * Contains classes for loading and updating achievements. + */ +package com.google.android.gms.games.achievement; diff --git a/play-services-games/src/main/java/com/google/android/gms/games/client/PlayGamesConsistencyTokens.java b/play-services-games/src/main/java/com/google/android/gms/games/client/PlayGamesConsistencyTokens.java new file mode 100644 index 0000000000..51071a0265 --- /dev/null +++ b/play-services-games/src/main/java/com/google/android/gms/games/client/PlayGamesConsistencyTokens.java @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.games.client; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.NonNull; +import org.microg.gms.utils.ToStringHelper; + +public class PlayGamesConsistencyTokens implements Parcelable { + public final String oneupConsistencyToken; + public final String superGlueConsistencyToken; + + public PlayGamesConsistencyTokens(String oneupConsistencyToken, String superGlueConsistencyToken) { + this.oneupConsistencyToken = oneupConsistencyToken; + this.superGlueConsistencyToken = superGlueConsistencyToken; + } + + @NonNull + @Override + public String toString() { + return ToStringHelper.name("PlayGamesConsistencyTokens") + .field("oneupConsistencyToken", oneupConsistencyToken) + .field("superGlueConsistencyToken", superGlueConsistencyToken) + .end(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString(oneupConsistencyToken); + dest.writeString(superGlueConsistencyToken); + } + + public static final Creator CREATOR = new Creator() { + @Override + public PlayGamesConsistencyTokens createFromParcel(Parcel source) { + return new PlayGamesConsistencyTokens(source.readString(), source.readString()); + } + + @Override + public PlayGamesConsistencyTokens[] newArray(int size) { + return new PlayGamesConsistencyTokens[size]; + } + }; +} diff --git a/play-services-games/src/main/java/com/google/android/gms/games/internal/connect/GamesSignInRequest.java b/play-services-games/src/main/java/com/google/android/gms/games/internal/connect/GamesSignInRequest.java new file mode 100644 index 0000000000..5cf2d0bd16 --- /dev/null +++ b/play-services-games/src/main/java/com/google/android/gms/games/internal/connect/GamesSignInRequest.java @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.games.internal.connect; + +import androidx.annotation.NonNull; +import org.microg.gms.utils.ToStringHelper; +import org.microg.safeparcel.AutoSafeParcelable; + +public class GamesSignInRequest extends AutoSafeParcelable { + @Field(1) + public int signInType; + @Field(2) + public SignInResolutionResult previousStepResolutionResult; + + @NonNull + @Override + public String toString() { + return ToStringHelper.name("GamesSignInRequest") + .field("signInType", signInType) + .field("previousStepResolutionResult", previousStepResolutionResult) + .end(); + } + + public static final Creator CREATOR = findCreator(GamesSignInRequest.class); +} diff --git a/play-services-games/src/main/java/com/google/android/gms/games/internal/connect/GamesSignInResponse.java b/play-services-games/src/main/java/com/google/android/gms/games/internal/connect/GamesSignInResponse.java new file mode 100644 index 0000000000..05a65b33fc --- /dev/null +++ b/play-services-games/src/main/java/com/google/android/gms/games/internal/connect/GamesSignInResponse.java @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.games.internal.connect; + +import org.microg.safeparcel.AutoSafeParcelable; + +public class GamesSignInResponse extends AutoSafeParcelable { + @Field(1) + public String gameRunToken; + public static final Creator CREATOR = findCreator(GamesSignInResponse.class); +} diff --git a/play-services-games/src/main/java/com/google/android/gms/games/internal/connect/SignInResolutionResult.java b/play-services-games/src/main/java/com/google/android/gms/games/internal/connect/SignInResolutionResult.java new file mode 100644 index 0000000000..9b3ccf15fc --- /dev/null +++ b/play-services-games/src/main/java/com/google/android/gms/games/internal/connect/SignInResolutionResult.java @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.games.internal.connect; + +import android.content.Intent; +import androidx.annotation.NonNull; +import org.microg.gms.utils.ToStringHelper; +import org.microg.safeparcel.AutoSafeParcelable; + +public class SignInResolutionResult extends AutoSafeParcelable { + @Field(1) + public Intent resultData; + + @NonNull + @Override + public String toString() { + return ToStringHelper.name("SignInResolutionResult").field("resultData", resultData).end(); + } + + public static final Creator CREATOR = findCreator(SignInResolutionResult.class); +} diff --git a/play-services-games/src/main/java/com/google/android/gms/games/internal/player/MostRecentGameInfo.java b/play-services-games/src/main/java/com/google/android/gms/games/internal/player/MostRecentGameInfo.java new file mode 100644 index 0000000000..ee60b0d3d1 --- /dev/null +++ b/play-services-games/src/main/java/com/google/android/gms/games/internal/player/MostRecentGameInfo.java @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.games.internal.player; + +import android.net.Uri; +import android.os.Parcelable; +import com.google.android.gms.common.data.Freezable; + +public interface MostRecentGameInfo extends Freezable, Parcelable { + long getActivityTimestampMillis(); + + Uri getGameFeaturedImageUri(); + + Uri getGameHiResImageUri(); + + Uri getGameIconImageUri(); + + String getGameId(); + + String getGameName(); +} diff --git a/play-services-games/src/main/java/com/google/android/gms/games/internal/player/MostRecentGameInfoEntity.java b/play-services-games/src/main/java/com/google/android/gms/games/internal/player/MostRecentGameInfoEntity.java new file mode 100644 index 0000000000..59a58f9f08 --- /dev/null +++ b/play-services-games/src/main/java/com/google/android/gms/games/internal/player/MostRecentGameInfoEntity.java @@ -0,0 +1,82 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.games.internal.player; + +import android.net.Uri; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import org.microg.gms.common.Hide; +import org.microg.safeparcel.AutoSafeParcelable; + +public class MostRecentGameInfoEntity extends AutoSafeParcelable implements MostRecentGameInfo { + @Field(1) + private String gameId; + @Field(2) + private String gameName; + @Field(3) + private long activityTimestampMillis; + @Field(4) + private Uri gameIconImageUri; + @Field(5) + private Uri gameHiResImageUri; + @Field(6) + private Uri gameFeaturedImageUri; + + @Hide + public MostRecentGameInfoEntity() { + } + + @Hide + public MostRecentGameInfoEntity(String gameId, String gameName, long activityTimestampMillis, Uri gameIconImageUri, Uri gameHiResImageUri, Uri gameFeaturedImageUri) { + this.gameId = gameId; + this.gameName = gameName; + this.activityTimestampMillis = activityTimestampMillis; + this.gameIconImageUri = gameIconImageUri; + this.gameHiResImageUri = gameHiResImageUri; + this.gameFeaturedImageUri = gameFeaturedImageUri; + } + + @Override + public long getActivityTimestampMillis() { + return activityTimestampMillis; + } + + @Override + public Uri getGameFeaturedImageUri() { + return gameFeaturedImageUri; + } + + @Override + public Uri getGameHiResImageUri() { + return gameHiResImageUri; + } + + @Override + public Uri getGameIconImageUri() { + return gameIconImageUri; + } + + @Override + public String getGameId() { + return gameId; + } + + @Override + public String getGameName() { + return gameName; + } + + @Override + public boolean isDataValid() { + return true; + } + + @Override + public MostRecentGameInfo freeze() { + return this; + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(MostRecentGameInfoEntity.class); +} diff --git a/play-services-games/src/main/java/com/google/android/gms/games/leaderboard/package-info.java b/play-services-games/src/main/java/com/google/android/gms/games/leaderboard/package-info.java new file mode 100644 index 0000000000..197c499713 --- /dev/null +++ b/play-services-games/src/main/java/com/google/android/gms/games/leaderboard/package-info.java @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: CC-BY-4.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ +/** + * Contains data classes for leaderboards. + */ +package com.google.android.gms.games.leaderboard; diff --git a/play-services-games/src/main/java/com/google/android/gms/games/multiplayer/realtime/RealTimeMessage.java b/play-services-games/src/main/java/com/google/android/gms/games/multiplayer/realtime/RealTimeMessage.java new file mode 100644 index 0000000000..0b7b393e57 --- /dev/null +++ b/play-services-games/src/main/java/com/google/android/gms/games/multiplayer/realtime/RealTimeMessage.java @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: 2019 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.games.multiplayer.realtime; + +import android.os.Parcel; +import android.os.Parcelable; +import org.microg.gms.common.Hide; + +@Hide +public final class RealTimeMessage implements Parcelable { + public static final int RELIABLE = 1; + public static final int UNRELIABLE = 0; + + private final String senderParticipantId; + private final byte[] messageData; + private final int reliable; + + public RealTimeMessage(String senderParticipantId, byte[] messageData, int reliable) { + this.senderParticipantId = senderParticipantId; + this.messageData = messageData.clone(); + this.reliable = reliable; + } + + private RealTimeMessage(Parcel parcel) { + this(parcel.readString(), parcel.createByteArray(), parcel.readInt()); + } + + public static final Creator CREATOR = new Creator() { + @Override + public RealTimeMessage createFromParcel(Parcel in) { + return new RealTimeMessage(in); + } + @Override + public RealTimeMessage[] newArray(int size) { + return new RealTimeMessage[size]; + } + }; + + public byte[] getMessageData() { + return this.messageData; + } + + public String getSenderParticipantId() { + return this.senderParticipantId; + } + + public boolean isReliable() { + return this.reliable == RELIABLE; + } + + @Override + public void writeToParcel(Parcel parcel, int flag) { + parcel.writeString(this.senderParticipantId); + parcel.writeByteArray(this.messageData); + parcel.writeInt(this.reliable); + } + + @Override + public int describeContents() { + return 0; + } +} diff --git a/play-services-games/src/main/java/com/google/android/gms/games/package-info.java b/play-services-games/src/main/java/com/google/android/gms/games/package-info.java new file mode 100644 index 0000000000..0b8e49ea9e --- /dev/null +++ b/play-services-games/src/main/java/com/google/android/gms/games/package-info.java @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: CC-BY-4.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ +/** + * Contains the games client class. + */ +package com.google.android.gms.games; diff --git a/play-services-games/src/main/java/com/google/android/gms/games/snapshot/package-info.java b/play-services-games/src/main/java/com/google/android/gms/games/snapshot/package-info.java new file mode 100644 index 0000000000..c33b19cf3a --- /dev/null +++ b/play-services-games/src/main/java/com/google/android/gms/games/snapshot/package-info.java @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: CC-BY-4.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ +/** + * Contains data classes for snapshot functionality. + */ +package com.google.android.gms.games.snapshot; diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataEventBuffer.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataEventBuffer.java index 1c6c98e04d..742ca19598 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataEventBuffer.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataEventBuffer.java @@ -18,6 +18,7 @@ import com.google.android.gms.common.api.Result; import com.google.android.gms.common.api.Status; +import com.google.android.gms.common.data.AbstractDataBuffer; import com.google.android.gms.common.data.DataBuffer; import com.google.android.gms.common.data.DataHolder; @@ -27,7 +28,7 @@ * Data structure holding references to a set of events. */ @PublicApi -public class DataEventBuffer extends DataBuffer implements Result { +public class DataEventBuffer extends AbstractDataBuffer implements Result { private Status status; @PublicApi(exclude = true) diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataItemBuffer.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataItemBuffer.java index 703d7ae3e0..54aeb16aaf 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataItemBuffer.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataItemBuffer.java @@ -18,13 +18,14 @@ import com.google.android.gms.common.api.Result; import com.google.android.gms.common.api.Status; +import com.google.android.gms.common.data.AbstractDataBuffer; import com.google.android.gms.common.data.DataBuffer; import com.google.android.gms.common.data.DataHolder; import org.microg.gms.common.PublicApi; @PublicApi -public class DataItemBuffer extends DataBuffer implements Result { +public class DataItemBuffer extends AbstractDataBuffer implements Result { private Status status; @PublicApi(exclude = true) diff --git a/settings.gradle b/settings.gradle index cdf4df4813..41c1af1d87 100644 --- a/settings.gradle +++ b/settings.gradle @@ -22,6 +22,7 @@ include ':play-services-clearcut' include ':play-services-drive' include ':play-services-droidguard' include ':play-services-fido' +include ':play-services-games' include ':play-services-gcm' include ':play-services-gmscompliance' include ':play-services-iid'