diff --git a/play-services-auth-base/build.gradle b/play-services-auth-base/build.gradle index 92ed11c76a..32c0565e0e 100644 --- a/play-services-auth-base/build.gradle +++ b/play-services-auth-base/build.gradle @@ -39,4 +39,6 @@ dependencies { api project(':play-services-basement') api project(':play-services-base') api project(':play-services-tasks') + + annotationProcessor project(':safe-parcel-processor') } diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/utils/BitmapUtils.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/utils/BitmapUtils.kt new file mode 100644 index 0000000000..6ebf8a37ff --- /dev/null +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/utils/BitmapUtils.kt @@ -0,0 +1,33 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.utils + +import android.graphics.Bitmap +import kotlin.math.sqrt + +object BitmapUtils { + + fun getBitmapSize(bitmap: Bitmap?): Int { + if (bitmap != null) { + return bitmap.height * bitmap.rowBytes + } + return 0 + } + + fun scaledBitmap(bitmap: Bitmap, maxSize: Float): Bitmap { + val height: Int = bitmap.getHeight() + val width: Int = bitmap.getWidth() + val sqrt = + sqrt(((maxSize) / ((width.toFloat()) / (height.toFloat()) * ((bitmap.getRowBytes() / width).toFloat()))).toDouble()) + .toInt() + return Bitmap.createScaledBitmap( + bitmap, + (((sqrt.toFloat()) / (height.toFloat()) * (width.toFloat())).toInt()), + sqrt, + true + ) + } +} \ No newline at end of file diff --git a/play-services-api/src/main/aidl/com/google/android/gms/common/data/BitmapTeleporter.aidl b/play-services-base/src/main/aidl/com/google/android/gms/common/data/BitmapTeleporter.aidl similarity index 100% rename from play-services-api/src/main/aidl/com/google/android/gms/common/data/BitmapTeleporter.aidl rename to play-services-base/src/main/aidl/com/google/android/gms/common/data/BitmapTeleporter.aidl diff --git a/play-services-api/src/main/java/com/google/android/gms/common/data/BitmapTeleporter.java b/play-services-base/src/main/java/com/google/android/gms/common/data/BitmapTeleporter.java similarity index 100% rename from play-services-api/src/main/java/com/google/android/gms/common/data/BitmapTeleporter.java rename to play-services-base/src/main/java/com/google/android/gms/common/data/BitmapTeleporter.java 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 index e347fa0e8b..46bd845899 100644 --- 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 @@ -9,6 +9,28 @@ package com.google.android.gms.common.images; import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.widget.ImageView; + +import androidx.annotation.NonNull; +import androidx.annotation.WorkerThread; +import androidx.collection.LruCache; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; /** * This class is used to load images from the network and handles local caching for you. @@ -21,6 +43,157 @@ public class ImageManager { * @return A new ImageManager. */ public static ImageManager create(Context context) { - throw new UnsupportedOperationException(); + if (INSTANCE == null) { + synchronized (ImageManager.class) { + if (INSTANCE == null) { + INSTANCE = new ImageManager(context); + } + } + } + return INSTANCE; + } + + public static final String TAG = "ImageManager"; + private static volatile ImageManager INSTANCE; + private final LruCache memoryCache; + private final ExecutorService executorService; + private final Handler handler; + private final Context context; + + private ImageManager(Context context) { + this.context = context.getApplicationContext(); + this.handler = new Handler(Looper.getMainLooper()); + this.executorService = Executors.newFixedThreadPool(4); + + final int cacheSize = (int) (Runtime.getRuntime().maxMemory() / 1024 / 8); + this.memoryCache = new LruCache(cacheSize) { + @Override + protected int sizeOf(@NonNull String key, @NonNull Bitmap bitmap) { + return bitmap.getByteCount() / 1024; + } + }; + } + + /** + * Compress Bitmap + */ + public byte[] compressBitmap(Bitmap bitmap, Bitmap.CompressFormat format, int quality) { + Log.d(TAG, "compressBitmap width: " + bitmap.getWidth() + " height:" + bitmap.getHeight()); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + bitmap.compress(format, quality, byteArrayOutputStream); + byte[] bitmapBytes = byteArrayOutputStream.toByteArray(); + bitmap = BitmapFactory.decodeByteArray(bitmapBytes, 0, bitmapBytes.length); + Log.d(TAG, "compressBitmap compress width: " + bitmap.getWidth() + " height:" + bitmap.getHeight()); + return bitmapBytes; + } + + public byte[] compressBitmap(Bitmap original, int newWidth, int newHeight) { + Log.d(TAG, "compressBitmap width: " + original.getWidth() + " height:" + original.getHeight()); + Bitmap target = Bitmap.createScaledBitmap(original, newWidth, newHeight, true); + Log.d(TAG, "compressBitmap target width: " + target.getWidth() + " height:" + target.getHeight()); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + target.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream); + return byteArrayOutputStream.toByteArray(); + } + + public void loadImage(final String url, final ImageView imageView) { + if (imageView == null) { + Log.d(TAG, "loadImage: imageView is null"); + return; + } + final Bitmap cachedBitmap = getBitmapFromCache(url); + if (cachedBitmap != null) { + Log.d(TAG, "loadImage from cached"); + imageView.setImageBitmap(cachedBitmap); + } else { + Log.d(TAG, "loadImage from net"); + imageView.setTag(url); + executorService.submit(() -> { + final Bitmap bitmap = downloadBitmap(url); + if (bitmap != null) { + addBitmapToCache(url, bitmap); + if (imageView.getTag().equals(url)) { + handler.post(() -> imageView.setImageBitmap(bitmap)); + } + } + }); + } + } + + private Bitmap getBitmapFromCache(String key) { + Bitmap bitmap = memoryCache.get(key); + if (bitmap == null) { + bitmap = getBitmapFromDiskCache(key); + } + return bitmap; + } + + private void addBitmapToCache(String key, Bitmap bitmap) { + if (getBitmapFromCache(key) == null) { + memoryCache.put(key, bitmap); + addBitmapToDiskCache(key, bitmap); + } + } + + private Bitmap getBitmapFromDiskCache(String key) { + File file = getDiskCacheFile(key); + if (file.exists()) { + return BitmapFactory.decodeFile(file.getAbsolutePath()); + } + return null; + } + + private void addBitmapToDiskCache(String key, Bitmap bitmap) { + File file = getDiskCacheFile(key); + try (FileOutputStream outputStream = new FileOutputStream(file)) { + bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream); + } catch (IOException e) { + Log.e(TAG, "addBitmapToDiskCache: ", e); + } } + + private File getDiskCacheFile(String key) { + File cacheDir = context.getCacheDir(); + return new File(cacheDir, md5(key)); + } + + private String md5(String s) { + try { + MessageDigest digest = MessageDigest.getInstance("MD5"); + digest.update(s.getBytes()); + byte[] messageDigest = digest.digest(); + StringBuilder hexString = new StringBuilder(); + for (byte b : messageDigest) { + StringBuilder h = new StringBuilder(Integer.toHexString(0xFF & b)); + while (h.length() < 2) h.insert(0, "0"); + hexString.append(h); + } + return hexString.toString(); + } catch (NoSuchAlgorithmException e) { + Log.e(TAG, "md5: ", e); + } + return ""; + } + + @WorkerThread + private Bitmap downloadBitmap(String url) { + HttpURLConnection connection = null; + try { + connection = (HttpURLConnection) new URL(url).openConnection(); + connection.connect(); + if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { + InputStream inputStream = connection.getInputStream(); + return BitmapFactory.decodeStream(inputStream); + } + } catch (IOException e) { + Log.d(TAG, "downloadBitmap: ", e); + } finally { + if (connection != null) { + connection.disconnect(); + } + } + return null; + } + + } diff --git a/play-services-base/src/main/java/com/google/android/gms/common/util/IOUtils.java b/play-services-base/src/main/java/com/google/android/gms/common/util/IOUtils.java new file mode 100644 index 0000000000..d57225a2b5 --- /dev/null +++ b/play-services-base/src/main/java/com/google/android/gms/common/util/IOUtils.java @@ -0,0 +1,101 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package com.google.android.gms.common.util; + +import android.os.ParcelFileDescriptor; + +import androidx.annotation.NonNull; + +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public final class IOUtils { + private IOUtils() { + } + + public static void closeQuietly(ParcelFileDescriptor parcelFileDescriptor) { + if (parcelFileDescriptor != null) { + try { + parcelFileDescriptor.close(); + } catch (IOException unused) { + } + } + } + + public static long copyStream(@NonNull InputStream inputStream, @NonNull OutputStream outputStream) throws IOException { + return copyStream(inputStream, outputStream, false, 1024); + } + + public static boolean isGzipByteBuffer(@NonNull byte[] bArr) { + if (bArr.length > 1) { + if ((((bArr[1] & 255) << 8) | (bArr[0] & 255)) == 35615) { + return true; + } + } + return false; + } + + public static byte[] readInputStreamFully(@NonNull InputStream inputStream) throws IOException { + return readInputStreamFully(inputStream, true); + } + + public static byte[] toByteArray(@NonNull InputStream inputStream) throws IOException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + byte[] bArr = new byte[4096]; + while (true) { + int read = inputStream.read(bArr); + if (read == -1) { + return byteArrayOutputStream.toByteArray(); + } + byteArrayOutputStream.write(bArr, 0, read); + } + } + + + public static void closeQuietly(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException unused) { + } + } + } + + public static long copyStream(@NonNull InputStream inputStream, @NonNull OutputStream outputStream, boolean z, int i) throws IOException { + byte[] bArr = new byte[i]; + long j = 0; + while (true) { + try { + int read = inputStream.read(bArr, 0, i); + if (read == -1) { + break; + } + j += read; + outputStream.write(bArr, 0, read); + } catch (Throwable th) { + if (z) { + closeQuietly(inputStream); + closeQuietly(outputStream); + } + throw th; + } + } + if (z) { + closeQuietly(inputStream); + closeQuietly(outputStream); + } + return j; + } + + public static byte[] readInputStreamFully(@NonNull InputStream inputStream, boolean z) throws IOException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + copyStream(inputStream, byteArrayOutputStream, z, 1024); + return byteArrayOutputStream.toByteArray(); + } + +} diff --git a/play-services-core-proto/build.gradle b/play-services-core-proto/build.gradle index f3f4a16b88..961fdfa551 100644 --- a/play-services-core-proto/build.gradle +++ b/play-services-core-proto/build.gradle @@ -8,6 +8,7 @@ apply plugin: 'kotlin' dependencies { implementation "com.squareup.wire:wire-runtime:$wireVersion" + api "com.squareup.wire:wire-grpc-client:$wireVersion" } wire { diff --git a/play-services-core-proto/src/main/proto/snapshot.proto b/play-services-core-proto/src/main/proto/snapshot.proto new file mode 100644 index 0000000000..60fc7d2f82 --- /dev/null +++ b/play-services-core-proto/src/main/proto/snapshot.proto @@ -0,0 +1,126 @@ +package google.play.games.games.v1; + +option java_outer_classname = "SnapshotProto"; + +option java_package = "org.microg.gms.games"; +option java_multiple_files = true; + +service SnapshotsExtended { + rpc SyncSnapshots (GetSnapshotRequest) returns (GetSnapshotResponse); + rpc DeleteSnapshot (DeleteSnapshotInfo) returns (EmptyResult); + rpc ResolveSnapshotHead(ResolveSnapshotHeadRequest) returns (ResolveSnapshotHeadResponse); + rpc PrepareSnapshotRevision(PrepareSnapshotRevisionRequest) returns (PrepareSnapshotRevisionResponse); + rpc CommitSnapshotRevision(CommitSnapshotRevisionRequest) returns (EmptyResult); +} + +message ResolveSnapshotHeadResponse { + optional SnapshotMetadata snapshotMetadata = 1; +} + +message PrepareSnapshotRevisionRequest { + optional string title = 1; + repeated ukq c = 2; + optional string randomUUID = 3; +} + +message PrepareSnapshotRevisionResponse { + optional string title = 1; + repeated UploadLinkInfo uploadLinkInfos = 2; +} + +message CommitSnapshotRevisionRequest { + optional string snapshotName = 1; + optional Snapshot snapshot = 3; + optional string unknownFileString2 = 2; + repeated string unknownFileString4 = 4; + optional string randomUUID = 5; + optional string oneofField6 = 6; + optional int32 unknownFileInt7 = 7; +} + +message UploadLinkInfo { + optional int32 id = 2; + optional string url = 3; + optional int32 unknownFileInt4 = 4; +} + +message ukq { + optional int32 unknownFileInt1 = 1; + optional int32 unknownFileInt2 = 2; +} + +message ResolveSnapshotHeadRequest { + optional string snapshotName = 1; + optional int32 unknownFileInt2 = 2; + optional int32 unknownFileInt3 = 3; +} + +message GetSnapshotRequest { + repeated int32 unknownFileIntList3 = 3; + optional int32 unknownFileInt4 = 4; + optional int32 unknownFileInt6 = 6; +} + +message DeleteSnapshotInfo { + optional string snapshotName = 1; + optional string snapshotId = 2; +} + +message EmptyResult { + +} + +message GetSnapshotResponse { + repeated GameSnapshot gameSnapshot = 1; + optional string dataSnapshot = 2; + optional string unknownFileString3 = 3; + optional int32 unknownFileInt4 = 4; +} + +message GameSnapshot { + optional SnapshotMetadata metadata = 1; + optional int32 type = 2; +} + +message SnapshotMetadata { + optional string snapshotName = 1; + optional Snapshot snapshot = 2; + optional int32 type = 3; + repeated Snapshot snapshots = 4; +} + +message Snapshot { + optional string snapshotId = 1; + optional SnapshotContent content = 2; + optional SnapshotContentInfo snapshotContentInfo = 3; + optional SnapshotImage coverImage = 4; +} + +message SnapshotContent { + optional string description = 2; + optional SnapshotTimeInfo snapshotTimeInfo = 3; + optional int64 progressValue = 5; + optional string deviceName = 6; + optional int64 duration = 7; +} + +message SnapshotTimeInfo { + required int64 timestamp = 1; + required int32 playedTime = 2; +} + +message SnapshotContentInfo { + optional string token = 1; + optional string url = 2; + optional string contentHash = 3; + optional int64 size = 4; +} + +message SnapshotImage { + optional string token = 1; + optional string imageUrl = 2; + optional int32 width = 3; + optional int32 height = 4; + optional string contentHash = 5; + optional string mimeType = 6; +} \ No newline at end of file diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index 4c71e4ba1b..cfcfc004cf 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -588,6 +588,23 @@ + + + + + + + + + + Unit = {}) = Intent(action).apply { - setPackage(GAMES_PACKAGE_NAME) + // Jump to internal page implementation + setPackage(Constants.GMS_PACKAGE_NAME) + putExtra(EXTRA_ACCOUNT_KEY, Integer.toHexString(account.name.hashCode())) putExtra(EXTRA_GAME_PACKAGE_NAME, packageName) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) block() @@ -491,6 +580,58 @@ class GamesServiceImpl(val context: Context, override val lifecycle: Lifecycle, override fun getPlayerSearchIntent(): Intent = getGamesIntent(ACTION_PLAYER_SEARCH) + override fun getSelectSnapshotIntent( + title: String?, + allowAddButton: Boolean, + allowDelete: Boolean, + maxSnapshots: Int + ): Intent { + Log.d(TAG, "Method getSelectSnapshotIntent($title, $allowAddButton, $allowDelete, $maxSnapshots) called") + return getGamesIntent(ACTION_VIEW_SNAPSHOTS) { + putExtra(EXTRA_TITLE, title) + putExtra(EXTRA_ALLOW_CREATE_SNAPSHOT, allowAddButton) + putExtra(EXTRA_ALLOW_DELETE_SNAPSHOT, allowDelete) + putExtra(EXTRA_MAX_SNAPSHOTS, maxSnapshots) + } + } + + override fun loadSnapshots(callbacks: IGamesCallbacks?, forceReload: Boolean) { + Log.d(TAG, "Method loadSnapshots(forceReload:$forceReload) called") + } + + override fun commitSnapshot( + callbacks: IGamesCallbacks?, + str: String?, + change: SnapshotMetadataChangeEntity?, + contents: Contents? + ) { + Log.d(TAG, "Method commitSnapshot(str:$str, change:$change, dvd:$contents)") + lifecycleScope.launchWhenStarted { + if (change != null && contents?.parcelFileDescriptor != null) { + runCatching { + val authResponse = withContext(Dispatchers.IO) { + AuthManager(context, account.name, packageName, "$SERVICE_GAMES_LITE ${Scopes.DRIVE_APPFOLDER}").apply { isPermitted = true }.requestAuth(true) + } + var oauthToken: String? = null + if (authResponse.auth?.let { oauthToken = it } == null) { + throw RuntimeException("oauthToken is null") + } + val result = SnapshotsDataClient.get(context).commitSnapshot(oauthToken!!, saveName, change, contents, maxCoverImageSize) + if (result == true) { + callbacks?.commitSnapshotResult(DataHolder.empty(GamesStatusCodes.OK.code)) + } else { + callbacks?.commitSnapshotResult(DataHolder.empty(GamesStatusCodes.SNAPSHOT_COMMIT_FAILED.code)) + } + }.onFailure { + Log.w(TAG, "commitSnapshot: error", it) + callbacks?.commitSnapshotResult(DataHolder.empty(GamesStatusCodes.SNAPSHOT_COMMIT_FAILED.code)) + } + } else { + callbacks?.commitSnapshotResult(DataHolder.empty(GamesStatusCodes.SNAPSHOT_COMMIT_FAILED.code)) + } + } + } + override fun loadEvents(callbacks: IGamesCallbacks?, forceReload: Boolean) { Log.d(TAG, "Not yet implemented: loadEvents($forceReload)") } @@ -499,20 +640,76 @@ class GamesServiceImpl(val context: Context, override val lifecycle: Lifecycle, Log.d(TAG, "Not yet implemented: incrementEvent($eventId, $incrementAmount)") } + override fun discardAndCloseSnapshot(contents: Contents?) { + Log.d(TAG, "discardAndCloseSnapshot: $contents") + } + override fun loadEventsById(callbacks: IGamesCallbacks?, forceReload: Boolean, eventsIds: Array?) { Log.d(TAG, "Not yet implemented: loadEventsById($forceReload, $eventsIds)") } override fun getMaxDataSize(): Int { + Log.d(TAG, "getMaxDataSize: ") return 3 * 1024 * 1024 } override fun getMaxCoverImageSize(): Int { + Log.d(TAG, "getMaxCoverImageSize: ") return 800 * 1024 } - override fun registerEventClient(callback: IGamesClient?, l: Long) { - Log.d(TAG, "Not yet implemented: registerEventClient($l)") + override fun resolveSnapshotHead(callbacks: IGamesCallbacks, saveName: String?, i: Int) { + Log.d(TAG, "Method resolveSnapshotHead $saveName, $i") + if (TextUtils.isEmpty(saveName)) { + Log.w(TAG, "resolveSnapshotHead: Must provide a non empty fileName!") + return + } + if (!pattern.matcher(saveName).matches()) { + Log.w(TAG, "resolveSnapshotHead: Must provide a valid file name!") + return + } + val driveId = DriveId(null, 30, 0, DriveId.RESOURCE_TYPE_FILE) + val file = File.createTempFile("blob", ".tmp", context.filesDir) + this.saveName = saveName + lifecycleScope.launchWhenStarted { + runCatching { + val authResponse = withContext(Dispatchers.IO) { + AuthManager(context, account.name, packageName, SERVICE_GAMES_LITE).apply { isPermitted = true }.requestAuth(true) + } + var oauthToken: String? = null + if (authResponse.auth?.let { oauthToken = it } == null) { + throw RuntimeException("oauthToken is null") + } + val resolveSnapshotHeadRequest = ResolveSnapshotHeadRequest.Builder().apply { + this.snapshotName = saveName + unknownFileInt2 = 5 + unknownFileInt3 = 3 + }.build() + val resolveSnapshotHeadResponse = SnapshotsDataClient.get(context).resolveSnapshotHead(oauthToken!!, resolveSnapshotHeadRequest) + val contentUrl = resolveSnapshotHeadResponse?.snapshotMetadata?.snapshot?.snapshotContentInfo?.url + if (contentUrl != null) { + val contentByteArray = SnapshotsDataClient.get(context).getDataFromDrive(oauthToken!!, contentUrl) + val fileOutputStream = FileOutputStream(file) + fileOutputStream.write(contentByteArray) + } + val columns = PlayerColumns.CURRENT_PLAYER_COLUMNS.toTypedArray() + + GameColumns.CURRENT_GAME_COLUMNS.toTypedArray() + + SnapshotColumns.CURRENT_GAME_COLUMNS.toTypedArray() + val dataHolder = if (player is PlayerEntity) { + DataHolder.builder(columns) + .withRow(player.toContentValues()).build(CommonStatusCodes.SUCCESS) + } else { + DataHolder.builder(columns).build(CommonStatusCodes.SIGN_IN_REQUIRED) + } + callbacks.onResolveSnapshotHead(dataHolder, Contents(ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE), 1, ParcelFileDescriptor.MODE_READ_WRITE, driveId, true, null)) + }.onFailure { + callbacks.onResolveSnapshotHead(DataHolder.empty(GamesStatusCodes.SNAPSHOT_COMMIT_FAILED.code), null) + } + } + } + + override fun registerEventClient(callback: IGamesClient?, clientId: Long) { + Log.d(TAG, "Not yet implemented: registerEventClient($clientId)") } private fun getCompareProfileIntent(playerId: String, block: Intent.() -> Unit = {}): Intent = getGamesIntent(ACTION_VIEW_PROFILE) { @@ -529,6 +726,15 @@ class GamesServiceImpl(val context: Context, override val lifecycle: Lifecycle, Log.d(TAG, "Not yet implemented: loadPlayerStats($forceReload)") } + override fun getLeaderboardsScoresIntent(leaderboardId: String?, timeSpan: Int, collection: Int): Intent { + Log.d(TAG, "Method getLeaderboardsScoresIntent Called: timeSpan:$timeSpan collection:$collection") + return getGamesIntent(ACTION_VIEW_LEADERBOARDS_SCORES) { + putExtra(EXTRA_LEADERBOARD_ID, leaderboardId) + putExtra(EXTRA_LEADERBOARD_TIME_SPAN, timeSpan) + putExtra(EXTRA_LEADERBOARD_COLLECTION, collection) + } + } + override fun getCurrentAccount(): Account? { Log.d(TAG, "Not yet implemented: getCurrentAccount") return account diff --git a/play-services-core/src/main/kotlin/org/microg/gms/games/achievements/AchievementResponseKt.kt b/play-services-core/src/main/kotlin/org/microg/gms/games/achievements/AchievementResponseKt.kt new file mode 100644 index 0000000000..6304a9267f --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/games/achievements/AchievementResponseKt.kt @@ -0,0 +1,194 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.games.achievements + +import org.json.JSONObject + +data class AchievementDefinitionsListResponse( + val items: List, val kind: String?, val nextPageToken: String? +) { + override fun toString(): String { + return "AchievementDefinitionsListResponse(items=$items, kind='$kind', nextPageToken='$nextPageToken')" + } +} + +data class AchievementDefinition( + val achievementType: Int, + val description: String?, + val experiencePoints: String, + val formattedTotalSteps: String?, + val id: String?, + var initialState: Int, + val isRevealedIconUrlDefault: Boolean?, + val isUnlockedIconUrlDefault: Boolean?, + val kind: String?, + val name: String?, + val revealedIconUrl: String?, + val totalSteps: Int, + val unlockedIconUrl: String? +) { + + constructor(name: String, type: Int) : this(type, null, "", "", "", 0, null, null, null, name, null, 0, null) + + override fun toString(): String { + return "AchievementDefinition(achievementType=$achievementType, description='$description', experiencePoints='$experiencePoints', formattedTotalSteps='$formattedTotalSteps', id='$id', initialState=$initialState, isRevealedIconUrlDefault=$isRevealedIconUrlDefault, isUnlockedIconUrlDefault=$isUnlockedIconUrlDefault, kind='$kind', name='$name', revealedIconUrl='$revealedIconUrl', totalSteps=$totalSteps, unlockedIconUrl='$unlockedIconUrl')" + } +} + +data class PlayerAchievement( + val kind: String?, + val id: String?, + val currentSteps: Int, + val formattedCurrentStepsString: String?, + val achievementState: String, + val lastUpdatedTimestamp: String?, + val experiencePoints: String? +) { + override fun toString(): String { + return "PlayerAchievement(kind=$kind, id=$id, currentSteps=$currentSteps, formattedCurrentStepsString=$formattedCurrentStepsString, achievementState=$achievementState, lastUpdatedTimestamp=$lastUpdatedTimestamp, experiencePoints=$experiencePoints)" + } +} + +data class AchievementIncrementResponse( + val kind: String?, val currentSteps: Int, val newlyUnlocked: Boolean +) { + override fun toString(): String { + return "AchievementIncrementResponse(kind=$kind, currentSteps=$currentSteps, newlyUnlocked=$newlyUnlocked)" + } +} + +data class AchievementRevealResponse( + val kind: String?, + val currentState: String, +) { + override fun toString(): String { + return "AchievementRevealResponse(kind=$kind, currentState=$currentState)" + } +} + +data class AchievementUnlockResponse( + val kind: String?, + val newlyUnlocked: Boolean, +) { + override fun toString(): String { + return "AchievementUnlockResponse(kind=$kind, newlyUnlocked=$newlyUnlocked)" + } +} + +fun JSONObject.toUnlockResponse(): AchievementUnlockResponse { + return AchievementUnlockResponse( + optString("kind"), optBoolean("newlyUnlocked") + ) +} + +fun JSONObject.toIncrementResponse(): AchievementIncrementResponse { + return AchievementIncrementResponse( + optString("kind"), optInt("currentSteps"), optBoolean("newlyUnlocked") + ) +} + +fun JSONObject.toRevealResponse(): AchievementRevealResponse { + return AchievementRevealResponse( + optString("kind"), optString("currentState") + ) +} + +fun JSONObject.toAllAchievementListResponse(): AchievementDefinitionsListResponse { + val items = optJSONArray("items") + val achievements = ArrayList() + if (items != null) { + for (i in 0.. { + val items = optJSONArray("items") + val achievements = ArrayList() + if (items != null) { + for (i in 0..