Repo Created

This commit is contained in:
Fr4nz D13trich 2025-11-15 17:44:12 +01:00
parent eb305e2886
commit a8c22c65db
4784 changed files with 329907 additions and 2 deletions

View file

@ -0,0 +1,45 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
// Metadata derived from play-services-auth-blockstore:16.4.0
apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
apply plugin: 'signing'
android {
namespace "com.google.android.gms.auth.blockstore"
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-auth-blockstore'
dependencies {
api project(':play-services-base')
api project(':play-services-basement')
api project(':play-services-tasks')
api 'org.jetbrains.kotlin:kotlin-stdlib:1.9.0'
annotationProcessor project(':safe-parcel-processor')
}

View file

@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
dependencies {
api project(':play-services-auth-blockstore')
implementation project(':play-services-base-core')
}
android {
namespace "org.microg.gms.auth.blockstore"
compileSdkVersion androidCompileSdk
buildToolsVersion "$androidBuildVersionTools"
defaultConfig {
versionName version
minSdkVersion androidMinSdk
targetSdkVersion androidTargetSdk
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
kotlinOptions {
jvmTarget = 1.8
}
lintOptions {
disable 'MissingTranslation', 'GetLocales'
}
}

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ SPDX-FileCopyrightText: 2025 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application>
<service android:name="org.microg.gms.auth.blockstore.BlockstoreApiService">
<intent-filter>
<action android:name="com.google.android.gms.auth.blockstore.service.START" />
</intent-filter>
</service>
</application>
</manifest>

View file

@ -0,0 +1,89 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.auth.blockstore
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import android.util.Base64
import android.util.Log
import com.google.android.gms.auth.blockstore.BlockstoreClient
import com.google.android.gms.auth.blockstore.BlockstoreStatusCodes
import com.google.android.gms.auth.blockstore.DeleteBytesRequest
import com.google.android.gms.auth.blockstore.RetrieveBytesRequest
import com.google.android.gms.auth.blockstore.RetrieveBytesResponse
import com.google.android.gms.auth.blockstore.StoreBytesData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.microg.gms.utils.toBase64
private const val SHARED_PREFS_NAME = "com.google.android.gms.blockstore"
private const val TAG = "BlockStoreImpl"
class BlockStoreImpl(context: Context, val callerPackage: String) {
private val blockStoreSp: SharedPreferences by lazy {
context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE)
}
private fun initSpByPackage(): Map<String, *>? {
val map = blockStoreSp.all
if (map.isNullOrEmpty() || map.all { !it.key.startsWith(callerPackage) }) return null
return map.filter { it.key.startsWith(callerPackage) }
}
suspend fun deleteBytesWithRequest(request: DeleteBytesRequest?): Boolean = withContext(Dispatchers.IO) {
Log.d(TAG, "deleteBytesWithRequest: callerPackage: $callerPackage")
val localData = initSpByPackage()
if (request == null || localData.isNullOrEmpty()) return@withContext false
if (request.deleteAll) {
localData.keys.forEach { blockStoreSp.edit()?.remove(it)?.commit() }
} else {
request.keys.forEach { blockStoreSp.edit()?.remove("$callerPackage:$it")?.commit() }
}
true
}
suspend fun retrieveBytesWithRequest(request: RetrieveBytesRequest?): RetrieveBytesResponse? = withContext(Dispatchers.IO) {
Log.d(TAG, "retrieveBytesWithRequest: callerPackage: $callerPackage")
val localData = initSpByPackage()
if (request == null || localData.isNullOrEmpty()) return@withContext null
val data = mutableListOf<RetrieveBytesResponse.BlockstoreData>()
val filterKeys = if (request.keys.isNullOrEmpty()) emptyList<String>() else request.keys
for (key in localData.keys) {
val bytesKey = key.substring(callerPackage.length + 1)
if (filterKeys.isNotEmpty() && !filterKeys.contains(bytesKey)) continue
val bytes = blockStoreSp.getString(key, null)?.let { Base64.decode(it, Base64.URL_SAFE) } ?: continue
data.add(RetrieveBytesResponse.BlockstoreData(bytes, bytesKey))
}
RetrieveBytesResponse(Bundle.EMPTY, data)
}
suspend fun retrieveBytes(): ByteArray? = withContext(Dispatchers.IO) {
Log.d(TAG, "retrieveBytes: callerPackage: $callerPackage")
val localData = initSpByPackage()
if (localData.isNullOrEmpty()) return@withContext null
val savedKey = localData.keys.firstOrNull { it == "$callerPackage:${BlockstoreClient.DEFAULT_BYTES_DATA_KEY}" } ?: return@withContext null
blockStoreSp.getString(savedKey, null)?.let { Base64.decode(it, Base64.URL_SAFE) }
}
suspend fun storeBytes(data: StoreBytesData?): Int = withContext(Dispatchers.IO) {
if (data == null || data.bytes == null) return@withContext 0
val localData = initSpByPackage()
if ((localData?.size ?: 0) >= BlockstoreClient.MAX_ENTRY_COUNT) {
return@withContext BlockstoreStatusCodes.TOO_MANY_ENTRIES
}
val bytes = data.bytes
if (bytes.size > BlockstoreClient.MAX_SIZE) {
return@withContext BlockstoreStatusCodes.MAX_SIZE_EXCEEDED
}
val savedKey = "$callerPackage:${data.key ?: BlockstoreClient.DEFAULT_BYTES_DATA_KEY}"
val base64 = bytes.toBase64(Base64.URL_SAFE)
val bool = blockStoreSp.edit()?.putString(savedKey, base64)?.commit()
if (bool == true) bytes.size else 0
}
}

View file

@ -0,0 +1,163 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.auth.blockstore
import android.os.Bundle
import android.util.Log
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.google.android.gms.auth.blockstore.AppRestoreInfo
import com.google.android.gms.auth.blockstore.BlockstoreStatusCodes
import com.google.android.gms.auth.blockstore.DeleteBytesRequest
import com.google.android.gms.auth.blockstore.RetrieveBytesRequest
import com.google.android.gms.auth.blockstore.RetrieveBytesResponse
import com.google.android.gms.auth.blockstore.StoreBytesData
import com.google.android.gms.auth.blockstore.internal.IBlockstoreService
import com.google.android.gms.auth.blockstore.internal.IDeleteBytesCallback
import com.google.android.gms.auth.blockstore.internal.IGetAccessForPackageCallback
import com.google.android.gms.auth.blockstore.internal.IGetBlockstoreDataCallback
import com.google.android.gms.auth.blockstore.internal.IIsEndToEndEncryptionAvailableCallback
import com.google.android.gms.auth.blockstore.internal.IRetrieveBytesCallback
import com.google.android.gms.auth.blockstore.internal.ISetBlockstoreDataCallback
import com.google.android.gms.auth.blockstore.internal.IStoreBytesCallback
import com.google.android.gms.common.Feature
import com.google.android.gms.common.api.CommonStatusCodes
import com.google.android.gms.common.api.Status
import com.google.android.gms.common.api.internal.IStatusCallback
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 kotlinx.coroutines.launch
import org.microg.gms.BaseService
import org.microg.gms.common.GmsService
import org.microg.gms.common.GmsService.BLOCK_STORE
import org.microg.gms.common.PackageUtils
private const val TAG = "BlockstoreApiService"
private val FEATURES = arrayOf(
Feature("auth_blockstore", 3),
Feature("blockstore_data_transfer", 1),
Feature("blockstore_notify_app_restore", 1),
Feature("blockstore_store_bytes_with_options", 2),
Feature("blockstore_is_end_to_end_encryption_available", 1),
Feature("blockstore_enable_cloud_backup", 1),
Feature("blockstore_delete_bytes", 2),
Feature("blockstore_retrieve_bytes_with_options", 3),
Feature("auth_clear_restore_credential", 2),
Feature("auth_create_restore_credential", 1),
Feature("auth_get_restore_credential", 1),
Feature("auth_get_private_restore_credential_key", 1),
Feature("auth_set_private_restore_credential_key", 1),
)
class BlockstoreApiService : BaseService(TAG, BLOCK_STORE) {
override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) {
try {
val packageName = PackageUtils.getAndCheckCallingPackage(this, request.packageName) ?: throw IllegalArgumentException("Missing package name")
val blockStoreImpl = BlockStoreImpl(this, packageName)
callback.onPostInitCompleteWithConnectionInfo(
CommonStatusCodes.SUCCESS, BlobstoreServiceImpl(blockStoreImpl, lifecycle).asBinder(), ConnectionInfo().apply { features = FEATURES })
} catch (e: Exception) {
Log.w(TAG, "handleServiceRequest", e)
callback.onPostInitComplete(CommonStatusCodes.INTERNAL_ERROR, null, null)
}
}
}
class BlobstoreServiceImpl(val blockStore: BlockStoreImpl, override val lifecycle: Lifecycle) : IBlockstoreService.Stub(), LifecycleOwner {
override fun retrieveBytes(callback: IRetrieveBytesCallback?) {
Log.d(TAG, "Method (retrieveBytes) called")
lifecycleScope.launch {
runCatching {
val retrieveBytes = blockStore.retrieveBytes()
if (retrieveBytes != null) {
callback?.onBytesResult(Status.SUCCESS, retrieveBytes)
} else {
callback?.onBytesResult(Status.INTERNAL_ERROR, null)
}
}
}
}
override fun setBlockstoreData(callback: ISetBlockstoreDataCallback?, data: ByteArray?) {
Log.d(TAG, "Method (setBlockstoreData: ${data?.size}) called but not implemented")
}
override fun getBlockstoreData(callback: IGetBlockstoreDataCallback?) {
Log.d(TAG, "Method (getBlockstoreData) called but not implemented")
}
override fun getAccessForPackage(callback: IGetAccessForPackageCallback?, packageName: String?) {
Log.d(TAG, "Method (getAccessForPackage: $packageName) called but not implemented")
}
override fun setFlagWithPackage(callback: IStatusCallback?, packageName: String?, flag: Int) {
Log.d(TAG, "Method (setFlagWithPackage: $packageName, $flag) called but not implemented")
}
override fun clearFlagForPackage(callback: IStatusCallback?, packageName: String?) {
Log.d(TAG, "Method (clearFlagForPackage: $packageName) called but not implemented")
}
override fun updateFlagForPackage(callback: IStatusCallback?, packageName: String?, value: Int) {
Log.d(TAG, "Method (updateFlagForPackage: $packageName, $value) called but not implemented")
}
override fun reportAppRestore(callback: IStatusCallback?, packages: List<String?>?, code: Int, info: AppRestoreInfo?) {
Log.d(TAG, "Method (reportAppRestore: $packages, $code, $info) called but not implemented")
}
override fun storeBytes(callback: IStoreBytesCallback?, data: StoreBytesData?) {
Log.d(TAG, "Method (storeBytes: $data) called")
lifecycleScope.launch {
runCatching {
val storeBytes = blockStore.storeBytes(data)
Log.d(TAG, "storeBytes: size: $storeBytes")
when (storeBytes) {
0 -> callback?.onStoreBytesResult(Status.INTERNAL_ERROR, BlockstoreStatusCodes.FEATURE_NOT_SUPPORTED)
BlockstoreStatusCodes.MAX_SIZE_EXCEEDED -> callback?.onStoreBytesResult(Status.INTERNAL_ERROR, BlockstoreStatusCodes.MAX_SIZE_EXCEEDED)
BlockstoreStatusCodes.TOO_MANY_ENTRIES -> callback?.onStoreBytesResult(Status.INTERNAL_ERROR, BlockstoreStatusCodes.TOO_MANY_ENTRIES)
else -> callback?.onStoreBytesResult(Status.SUCCESS, storeBytes)
}
}
}
}
override fun isEndToEndEncryptionAvailable(callback: IIsEndToEndEncryptionAvailableCallback?) {
Log.d(TAG, "Method (isEndToEndEncryptionAvailable) called")
runCatching { callback?.onCheckEndToEndEncryptionResult(Status.SUCCESS, false) }
}
override fun retrieveBytesWithRequest(callback: IRetrieveBytesCallback?, request: RetrieveBytesRequest?) {
Log.d(TAG, "Method (retrieveBytesWithRequest: $request) called")
lifecycleScope.launch {
runCatching {
val retrieveBytesResponse = blockStore.retrieveBytesWithRequest(request)
Log.d(TAG, "retrieveBytesWithRequest: retrieveBytesResponse: $retrieveBytesResponse")
if (retrieveBytesResponse != null) {
callback?.onResponseResult(Status.SUCCESS, retrieveBytesResponse)
} else {
callback?.onResponseResult(Status.INTERNAL_ERROR, RetrieveBytesResponse(Bundle.EMPTY, emptyList()))
}
}
}
}
override fun deleteBytes(callback: IDeleteBytesCallback?, request: DeleteBytesRequest?) {
Log.d(TAG, "Method (deleteBytes: $request) called")
lifecycleScope.launch {
runCatching {
val deleted = blockStore.deleteBytesWithRequest(request)
callback?.onDeleteBytesResult(Status.SUCCESS, deleted)
}
}
}
}

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2025 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<manifest />

View file

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.auth.blockstore;
parcelable AppRestoreInfo;

View file

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.auth.blockstore;
parcelable DeleteBytesRequest;

View file

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.auth.blockstore;
parcelable RetrieveBytesRequest;

View file

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.auth.blockstore;
parcelable RetrieveBytesResponse;

View file

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.auth.blockstore;
parcelable StoreBytesData;

View file

@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.auth.blockstore.internal;
import com.google.android.gms.common.api.Status;
import android.os.Bundle;
import com.google.android.gms.auth.blockstore.AppRestoreInfo;
import com.google.android.gms.auth.blockstore.DeleteBytesRequest;
import com.google.android.gms.auth.blockstore.RetrieveBytesRequest;
import com.google.android.gms.auth.blockstore.StoreBytesData;
import com.google.android.gms.common.api.internal.IStatusCallback;
import com.google.android.gms.auth.blockstore.internal.IRetrieveBytesCallback;
import com.google.android.gms.auth.blockstore.internal.ISetBlockstoreDataCallback;
import com.google.android.gms.auth.blockstore.internal.IGetBlockstoreDataCallback;
import com.google.android.gms.auth.blockstore.internal.IGetAccessForPackageCallback;
import com.google.android.gms.auth.blockstore.internal.IStoreBytesCallback;
import com.google.android.gms.auth.blockstore.internal.IIsEndToEndEncryptionAvailableCallback;
import com.google.android.gms.auth.blockstore.internal.IDeleteBytesCallback;
interface IBlockstoreService {
void retrieveBytes(IRetrieveBytesCallback callback) = 1;
void setBlockstoreData(ISetBlockstoreDataCallback callback, in byte[] data) = 2;
void getBlockstoreData(IGetBlockstoreDataCallback callback) = 3;
void getAccessForPackage(IGetAccessForPackageCallback callback, String packageName) = 4;
void setFlagWithPackage(IStatusCallback callback, String packageName, int flag) = 5;
void clearFlagForPackage(IStatusCallback callback, String packageName) = 6;
void updateFlagForPackage(IStatusCallback callback, String packageName, int value) = 7;
void reportAppRestore(IStatusCallback callback, in List<String> packages, int code, in AppRestoreInfo info) = 8;
void storeBytes(IStoreBytesCallback callback, in StoreBytesData data) = 9;
void isEndToEndEncryptionAvailable(IIsEndToEndEncryptionAvailableCallback callback) = 10;
void retrieveBytesWithRequest(IRetrieveBytesCallback callback, in RetrieveBytesRequest request) = 11;
void deleteBytes(IDeleteBytesCallback callback, in DeleteBytesRequest request) = 12;
}

View file

@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.auth.blockstore.internal;
import com.google.android.gms.common.api.Status;
interface IDeleteBytesCallback {
void onDeleteBytesResult(in Status status, boolean deleted);
}

View file

@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.auth.blockstore.internal;
import com.google.android.gms.common.api.Status;
interface IGetAccessForPackageCallback {
void onGetAccessResult(in Status status, int accessResult);
}

View file

@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.auth.blockstore.internal;
import com.google.android.gms.common.api.Status;
interface IGetBlockstoreDataCallback {
void onGetBlockstoreData(in Status status, in byte[] dataBytes);
}

View file

@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.auth.blockstore.internal;
import com.google.android.gms.common.api.Status;
interface IIsEndToEndEncryptionAvailableCallback {
void onCheckEndToEndEncryptionResult(in Status status, boolean available);
}

View file

@ -0,0 +1,14 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.auth.blockstore.internal;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.auth.blockstore.RetrieveBytesResponse;
interface IRetrieveBytesCallback {
void onBytesResult(in Status status, in byte[] data);
void onResponseResult(in Status status, in RetrieveBytesResponse response);
}

View file

@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.auth.blockstore.internal;
import com.google.android.gms.common.api.Status;
interface ISetBlockstoreDataCallback {
void onSetBytesResult(in Status status, int storedSize);
}

View file

@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.auth.blockstore.internal;
import com.google.android.gms.common.api.Status;
interface IStoreBytesCallback {
void onStoreBytesResult(in Status status, int result);
}

View file

@ -0,0 +1,46 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.auth.blockstore;
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;
import org.microg.gms.utils.ToStringHelper;
@SafeParcelable.Class
@Hide
public class AppRestoreInfo extends AbstractSafeParcelable {
@Field(value = 1)
public String restoreSessionId;
@Field(value = 2)
public String restoreSource;
@Constructor
public AppRestoreInfo(@Param(1) String restoreSessionId, @Param(2) String restoreSource) {
this.restoreSessionId = restoreSessionId;
this.restoreSource = restoreSource;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
CREATOR.writeToParcel(this, dest, flags);
}
@NonNull
@Override
public String toString() {
return ToStringHelper.name("AppRestoreInfo").field("restoreSessionId", restoreSessionId).field("restoreSource", restoreSource).end();
}
public static final SafeParcelableCreatorAndWriter<AppRestoreInfo> CREATOR = findCreator(AppRestoreInfo.class);
}

View file

@ -0,0 +1,26 @@
/*
* SPDX-FileCopyrightText: 2025 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.auth.blockstore;
import android.content.Context;
/**
* Entry point for Block Store API.
* <p>
* Allows apps to transfer small amounts of data via device-to-device restore. This enables a seamless sign-in when users start using a new
* device.
*/
public class Blockstore {
/**
* Creates a new instance of {@link BlockstoreClient}.
*/
public static BlockstoreClient getClient(Context context) {
throw new UnsupportedOperationException();
}
}

View file

@ -0,0 +1,119 @@
/*
* SPDX-FileCopyrightText: 2025 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.auth.blockstore;
import com.google.android.gms.common.api.Api;
import com.google.android.gms.common.api.HasApiKey;
import com.google.android.gms.tasks.Task;
/**
* The interface for clients to access Block Store.
* <ul>
* <li>
* Clients should call storeBytes(StoreBytesData) to store authentication credentials byte[] bytes to enable seamless sign-in on
* other devices.
* </li>
* <li>Clients should call retrieveBytes() to fetch the authentication credentials to seamlessly sign in users on a newly setup device.</li>
* </ul>
*/
public interface BlockstoreClient extends HasApiKey<Api.ApiOptions.NoOptions> {
/**
* The default key with which the bytes are associated when {@link #storeBytes(StoreBytesData)} is called without explicitly setting a {@code key}.
*/
String DEFAULT_BYTES_DATA_KEY = "com.google.android.gms.auth.blockstore.DEFAULT_BYTES_DATA_KEY";
/**
* Maximum number of distinct data entries, differentiated by the data keys, that can be stored using {@link BlockstoreClient#storeBytes(StoreBytesData)}.
* <p>
* The data key is the value provided when storing the data via {@link #storeBytes(StoreBytesData)}, as {@code StoreBytesData.key}.
*/
int MAX_ENTRY_COUNT = 16;
/**
* Maximum allowed size of byte blobs that can be stored using {@link BlockstoreClient#storeBytes(StoreBytesData)}.
*/
int MAX_SIZE = 1024;
/**
* Returns a {@link Task} which asynchronously deletes the bytes matching the filter(s) specified in {@code deleteBytesRequest}, with a Boolean result
* representing whether any bytes were actually deleted.
* <p>
* If no bytes were found to delete, the task succeeds with a {@code false} return value.
*
* @throws NullPointerException if {@code deleteBytesRequest} is null.
*/
Task<Boolean> deleteBytes(DeleteBytesRequest deleteBytesRequest) throws NullPointerException;
/**
* Returns a {@code Task} which asynchronously determines whether Block Store data backed up to the cloud will be end-to-end encrypted.
* <p>
* End-to-end encryption is available for Pie and above devices with a lockscreen PIN/pattern.
* <p>
* The {@code Boolean} return value is whether Block Store data backed up to the cloud will be end-to-end encrypted.
*/
Task<Boolean> isEndToEndEncryptionAvailable();
/**
* Returns a {@code Task} which asynchronously retrieves the previously-stored bytes, if any, matching the filter(s) specified in
* {@code retrieveBytesRequest}.
* <p>
* The returned {@code RetrieveBytesResponse} contains a map from data keys to {@code BlockstoreData}. The data may have been written on the same
* device or may have been transferred during the device setup.
* <p>
* Use this API to seamlessly sign-in users to the apps on a new device.
* <p>
* If no data is found, returns an empty data map. Note that the data may be cleared by Google Play services on certain user actions, like user
* clearing app storage (among others).
* <p>
* The bytes stored without an explicitly specified {@code StoreBytesData.key} can be requested with, and is returned associated with, the default
* key {@link #DEFAULT_BYTES_DATA_KEY}.
*
* @throws NullPointerException if {@code retrieveBytesRequest} is null.
*/
Task<RetrieveBytesResponse> retrieveBytes(RetrieveBytesRequest retrieveBytesRequest) throws NullPointerException;
/**
* Returns a {@link Task} which asynchronously retrieves the previously-stored bytes that was stored without an explicitly specified
* {@code StoreBytesData.key}, if any. The maximum size of the {@code byte[]} is the {@link #MAX_SIZE}.
* <p>
* The {@code byte[]} may have been written on the same device or may have been transferred during the device setup.
* <p>
* Use this API to seamlessly sign-in users to the apps on a new device.
* <p>
* If no data is found, returns an empty byte array. Note that the data may be cleared by Google Play services on certain user actions, like user
* clearing app storage (among others).
*
* @deprecated Use {@link #retrieveBytes(RetrieveBytesRequest)} instead.
*/
@Deprecated
Task<byte[]> retrieveBytes();
/**
* Returns a {@link Task} which asynchronously stores the provided {@code byte[] bytes} and associates it with the provided {@code String key}.
* <p>
* If the {@code key} is not explicitly set, then the {@code bytes} will be associated with the default key {@link #DEFAULT_BYTES_DATA_KEY}.
* <p>
* The data is stored locally. It is transferred to a new device during the device-to-device restore if a google account is also transferred.
* <p>
* If in {@link StoreBytesData#shouldBackupToCloud()} is set to {@code true}, the data will also be backed up to the cloud in the next periodic sync.
* Cloud backup data is transferred to a new device during the cloud restore using Google's Backup & Restore services.
* <p>
* The maximum size of {@code String key} and {@code byte[] bytes} combined is {@link #MAX_SIZE}; otherwise, the API fails with
* {@link BlockstoreStatusCodes#MAX_SIZE_EXCEEDED} error code.
* <p>
* The maximum number of data entries allowed is {@link #MAX_ENTRY_COUNT}; otherwise, the API fails with
* {@link BlockstoreStatusCodes#TOO_MANY_ENTRIES} error code.
* <p>
* The {@code Integer} return value is the size of {@code byte[] bytes} successfully stored.
* <p>
* Use this API to store small data blobs that can enable seamless sign in for your apps. The API may be called periodically (for example, a few
* times per day) to refresh the data blob. Successive calls with the same key to this API will overwrite the existing bytes.
*/
Task<Integer> storeBytes(StoreBytesData storeBytesData);
}

View file

@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: 2025 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.auth.blockstore;
import com.google.android.gms.common.api.CommonStatusCodes;
/**
* Block Store specific status codes.
* <p>
* Codes are allocated from the range 40000 to 40499, allocated in {@link CommonStatusCodes}.
*/
public class BlockstoreStatusCodes extends CommonStatusCodes {
/**
* The available quota was exceeded.
*/
public static final int MAX_SIZE_EXCEEDED = 40000;
/**
* Attempting to store a new key value pair after reaching the maximum number of entries allowed.
*/
public static final int TOO_MANY_ENTRIES = 40001;
/**
* Attempting to use a Blockstore feature that is not (yet) supported on the given device.
*/
public static final int FEATURE_NOT_SUPPORTED = 40002;
}

View file

@ -0,0 +1,138 @@
/*
* SPDX-FileCopyrightText: 2025 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.auth.blockstore;
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;
import org.microg.gms.utils.ToStringHelper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* A request to delete app data from BlockStore.
*/
@SafeParcelable.Class
public class DeleteBytesRequest extends AbstractSafeParcelable {
@Field(value = 1, getterName = "getKeys")
private final List<String> keys;
@Field(value = 2, getterName = "getDeleteAll")
private final boolean deleteAll;
@Constructor
DeleteBytesRequest(@Param(1) List<String> keys, @Param(2) boolean deleteAll) {
this.keys = keys;
this.deleteAll = deleteAll;
if (deleteAll && keys != null && !keys.isEmpty()) {
throw new IllegalArgumentException("deleteAll was set to true but keys were also provided");
}
for (String key : keys) {
if (key == null || key.isEmpty()) {
throw new IllegalArgumentException("Element in keys cannot be null or empty");
}
}
}
/**
* Returns the list of keys whose associated data, if any, should be deleted.
* <p>
* An empty list means that no key-based filtering will be performed. In other words, no data will be deleted if the key list is empty and no other
* criterion is provided.
* <p>
* Note that the app data that was stored without an explicit key can be deleted with the default key
* {@link BlockstoreClient#DEFAULT_BYTES_DATA_KEY}.
*/
@NonNull
public List<String> getKeys() {
return keys;
}
/**
* Returns whether or not all app's Block Store data should be deleted.
*/
public boolean getDeleteAll() {
return deleteAll;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
CREATOR.writeToParcel(this, dest, flags);
}
@NonNull
@Override
@Hide
public String toString() {
return ToStringHelper.name("DeleteBytesRequest").field("deleteAll", deleteAll).field("keys", keys).end();
}
public static final SafeParcelableCreatorAndWriter<DeleteBytesRequest> CREATOR = findCreator(DeleteBytesRequest.class);
/**
* A builder for {@link DeleteBytesRequest} objects.
*/
public static class Builder {
private final List<String> keyList = new ArrayList<>();
private boolean deleteAll = false;
/**
* Constructor for the {@link DeleteBytesRequest.Builder} class.
*/
public Builder() {
}
/**
* Builds and returns the {@link DeleteBytesRequest} object.
*/
public DeleteBytesRequest build() {
if (deleteAll && !keyList.isEmpty()) {
throw new IllegalStateException("deleteAll=true but keys are provided");
}
return new DeleteBytesRequest(new ArrayList<>(keyList), deleteAll);
}
/**
* Sets whether or not all app's Block Store data should be deleted.
* <p>
* The default is {@code false}.
* <p>
* Note that if {@code deleteAll} is set to true, then you should NOT set any other deletion criterion, e.g. {@code keys} should be empty. Otherwise, an
* IllegalStateException will be thrown.
*/
public Builder setDeleteAll(boolean deleteAll) {
this.deleteAll = deleteAll;
return this;
}
/**
* Sets the list of keys whose associated data, if any, should be deleted.
* <p>
* The default value is an empty list, which means that no key-based filtering will be performed. In other words, no data will be deleted if the
* key list is empty and no other criterion is provided.
* <p>
* Note that the app data that was stored without an explicit key can be deleted with the default key
* {@link BlockstoreClient#DEFAULT_BYTES_DATA_KEY}.
*/
public Builder setKeys(List<String> keys) {
keyList.clear();
keyList.addAll(keys);
return this;
}
}
}

View file

@ -0,0 +1,132 @@
/*
* SPDX-FileCopyrightText: 2025 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.auth.blockstore;
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.PublicApi;
import org.microg.gms.utils.ToStringHelper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* A request to retrieve app data from BlockStore.
*/
@SafeParcelable.Class
public class RetrieveBytesRequest extends AbstractSafeParcelable {
@Field(value = 1, getterName = "getKeys")
private final List<String> keys;
@Field(value = 2, getterName = "getRetrieveAll")
private final boolean retrieveAll;
@Constructor
RetrieveBytesRequest(@Param(1) List<String> keys, @Param(2) boolean retrieveAll) {
if (retrieveAll && keys != null && !keys.isEmpty()) {
throw new IllegalArgumentException("retrieveAll was set to true but other constraint(s) was also provided: keys");
}
this.retrieveAll = retrieveAll;
List<String> tmp = new ArrayList<>();
if (keys != null) {
for (String k : keys) {
if (k == null || k.isEmpty()) {
throw new IllegalArgumentException("Element in keys cannot be null or empty");
}
tmp.add(k);
}
}
this.keys = Collections.unmodifiableList(tmp);
}
/**
* Returns the list of keys whose associated data, if any, should be retrieved.
* <p>
* An empty list means that no key-based filtering will be performed. In other words, no data will be returned if the key list is empty and no
* other criterion is provided.
* <p>
* Note that the app data that was stored without an explicit key can be requested with the default key
* {@link BlockstoreClient#DEFAULT_BYTES_DATA_KEY}.
*/
public List<String> getKeys() {
return keys;
}
/**
* Returns whether or not all app's Block Store data should be retrieved.
*/
public boolean getRetrieveAll() {
return retrieveAll;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
CREATOR.writeToParcel(this, dest, flags);
}
@Override
public String toString() {
return ToStringHelper.name("RetrieveBytesRequest").field("keys", keys).field("retrieveAll", retrieveAll).end();
}
public static final SafeParcelableCreatorAndWriter<RetrieveBytesRequest> CREATOR = findCreator(RetrieveBytesRequest.class);
/**
* A builder for {@link RetrieveBytesRequest} objects.
*/
public static class Builder {
private final List<String> keys = new ArrayList<>();
private boolean retrieveAll = false;
/**
* Builds and returns the {@link RetrieveBytesRequest} object.
*/
@NonNull
public RetrieveBytesRequest build() {
return new RetrieveBytesRequest(new ArrayList<>(keys), retrieveAll);
}
/**
* Sets the list of keys whose associated data, if any, should be retrieved.
* <p>
* The default value is an empty list, which means that no key-based filtering will be performed. In other words, no data will be returned if the
* key list is empty and no other criterion is provided.
* <p>
* Note that the app data that was stored without an explicit key can be requested with the default key
* {@link BlockstoreClient#DEFAULT_BYTES_DATA_KEY}.
*/
public Builder setKeys(List<String> keys) {
this.keys.clear();
this.keys.addAll(keys);
return this;
}
/**
* Sets whether or not all app's Block Store data should be retrieved.
* <p>
* The default is {@code false}.
* <p>
* Note that if {@code retrieveAll} is set to true, then you should NOT set any other retrieval criterion, e.g. {@code keys} should be empty. Otherwise, an
* IllegalStateException will be thrown.
*/
public Builder retrieveAll(boolean retrieveAll) {
this.retrieveAll = retrieveAll;
return this;
}
}
}

View file

@ -0,0 +1,139 @@
/*
* SPDX-FileCopyrightText: 2025 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.auth.blockstore;
import android.os.Bundle;
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;
import org.microg.gms.utils.ToStringHelper;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* App data retrieved from BlockStore.
*/
@SafeParcelable.Class
public class RetrieveBytesResponse extends AbstractSafeParcelable {
@Deprecated
@Field(value = 1, getterName = "getInternalBlockstoreDataBundle")
private final Bundle internalBlockstoreDataBundle;
@Field(value = 2, getterName = "getInternalBlockstoreDataList")
private final List<BlockstoreData> internalBlockstoreDataList;
private final Map<String, BlockstoreData> blockstoreDataMap;
@Constructor
@Hide
public RetrieveBytesResponse(@Param(1) Bundle internalBlockstoreDataBundle, @Param(2) List<BlockstoreData> internalBlockstoreDataList) {
this.internalBlockstoreDataBundle = internalBlockstoreDataBundle;
this.internalBlockstoreDataList = internalBlockstoreDataList;
HashMap<String, BlockstoreData> blockstoreDataMap = new HashMap<>();
for (BlockstoreData blockstoreData : internalBlockstoreDataList) {
blockstoreDataMap.put(blockstoreData.key, blockstoreData);
}
this.blockstoreDataMap = blockstoreDataMap;
}
/**
* A mapping from app data key to {@link RetrieveBytesResponse.BlockstoreData} found based on a {@link RetrieveBytesRequest}.
* <p>
* The app data key is the value provided when storing the data via {@link BlockstoreClient#storeBytes(StoreBytesData)}, as
* {@code StoreBytesData.key}.
* <p>
* Note that the app data that was stored without an explicit key is associated with the default key
* {@link BlockstoreClient#DEFAULT_BYTES_DATA_KEY}.
*/
public Map<String, RetrieveBytesResponse.BlockstoreData> getBlockstoreDataMap() {
return blockstoreDataMap;
}
@Hide
public Bundle getInternalBlockstoreDataBundle() {
return internalBlockstoreDataBundle;
}
@Hide
public List<BlockstoreData> getInternalBlockstoreDataList() {
return internalBlockstoreDataList;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
CREATOR.writeToParcel(this, dest, flags);
}
@NonNull
@Override
@Hide
public String toString() {
return ToStringHelper.name("RetrieveBytesResponse").value(blockstoreDataMap).end();
}
public static final SafeParcelableCreatorAndWriter<RetrieveBytesResponse> CREATOR = findCreator(RetrieveBytesResponse.class);
/**
* A block of app data previously stored to Block Store.
*/
@SafeParcelable.Class
public static class BlockstoreData extends AbstractSafeParcelable {
@Field(value = 1, getterName = "getBytes")
@NonNull
private final byte[] bytes;
@Field(value = 2, getterName = "getKey")
@NonNull
private final String key;
@Constructor
@Hide
public BlockstoreData(@NonNull @Param(1) byte[] bytes, @NonNull @Param(2) String key) {
this.bytes = bytes;
this.key = key;
}
/**
* Raw bytes passed from the app to Block Store.
*/
@NonNull
public byte[] getBytes() {
return bytes;
}
@Hide
@NonNull
public String getKey() {
return key;
}
@NonNull
@Override
@Hide
public String toString() {
return ToStringHelper.name("BlockstoreData").value(key).field("bytes", bytes, true).end();
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
CREATOR.writeToParcel(this, dest, flags);
}
public static final SafeParcelableCreatorAndWriter<BlockstoreData> CREATOR = findCreator(BlockstoreData.class);
}
}

View file

@ -0,0 +1,134 @@
/*
* SPDX-FileCopyrightText: 2025 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.auth.blockstore;
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;
import org.microg.gms.utils.ToStringHelper;
/**
* Data passed by apps to Block Store.
*/
@SafeParcelable.Class
public class StoreBytesData extends AbstractSafeParcelable {
@Field(value = 1, getterName = "getBytes")
@NonNull
private final byte[] bytes;
@Field(value = 2, getterName = "shouldBackupToCloud")
private final boolean shouldBackupToCloud;
@Field(value = 3, getterName = "getKey")
private final String key;
@Constructor
StoreBytesData(@NonNull @Param(1) byte[] bytes, @Param(2) boolean shouldBackupToCloud, @Param(3) String key) {
this.bytes = bytes;
this.shouldBackupToCloud = shouldBackupToCloud;
this.key = key;
}
/**
* Raw bytes passed from apps to Block Store.
*/
@NonNull
public byte[] getBytes() {
return bytes;
}
/**
* The key with which the bytes are associated.
* <p>
* If the key was never explicitly set when building the {@code StoreBytesData}, then the default key {@link BlockstoreClient#DEFAULT_BYTES_DATA_KEY}
* is associated with the {@code bytes} and therefore will be returned.
*/
@NonNull
public String getKey() {
return key;
}
/**
* Whether the bytes to be stored should be backed up to the cloud in the next sync.
*/
public boolean shouldBackupToCloud() {
return shouldBackupToCloud;
}
public static final SafeParcelableCreatorAndWriter<StoreBytesData> CREATOR = findCreator(StoreBytesData.class);
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
CREATOR.writeToParcel(this, dest, flags);
}
@NonNull
@Override
@Hide
public String toString() {
return ToStringHelper.name("StoreBytesData").field("bytes", bytes != null ? bytes.length : 0).field("shouldBackupToCloud", shouldBackupToCloud).field("key", key).end();
}
/**
* A builder for {@link StoreBytesData} objects.
*/
public static class Builder {
private byte[] bytes;
private boolean shouldBackupToCloud = false;
private String key = BlockstoreClient.DEFAULT_BYTES_DATA_KEY;
/**
* Constructor for the {@link StoreBytesData.Builder} class.
*/
public Builder() {
}
/**
* Builds and returns the {@link StoreBytesData} object.
*/
public StoreBytesData build() {
return new StoreBytesData(bytes, shouldBackupToCloud, key);
}
/**
* Sets the raw bytes to be stored with Block Store. See {@link BlockstoreClient#MAX_SIZE} for the maximum size allowed for a key-bytes entry.
*/
public Builder setBytes(byte[] bytes) {
this.bytes = bytes;
return this;
}
/**
* Sets the key with which the {@code bytes} are associated with. See {@link BlockstoreClient#MAX_SIZE} for the maximum size allowed for a key-bytes
* entry.
* <p>
* If {@code setKey} is never invoked, the bytes will be associated with the default key {@link BlockstoreClient#DEFAULT_BYTES_DATA_KEY} when stored
* into Block Store.
*/
public Builder setKey(String key) {
this.key = key;
return this;
}
/**
* Sets whether the bytes to be stored should be backed up to the cloud in the next sync.
*/
public Builder setShouldBackupToCloud(boolean shouldBackupToCloud) {
this.shouldBackupToCloud = shouldBackupToCloud;
return this;
}
}
}

View file

@ -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 Block Store API.
*/
package com.google.android.gms.auth.blockstore;