Skip to content

Commit e29b524

Browse files
Blockstore: Add local-only implementation (#3036)
Co-authored-by: Marvin W <[email protected]>
1 parent 9c37566 commit e29b524

31 files changed

+1307
-2
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 microG Project Team
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
// Metadata derived from play-services-auth-blockstore:16.4.0
7+
8+
apply plugin: 'com.android.library'
9+
apply plugin: 'maven-publish'
10+
apply plugin: 'signing'
11+
12+
android {
13+
namespace "com.google.android.gms.auth.blockstore"
14+
15+
compileSdkVersion androidCompileSdk
16+
buildToolsVersion "$androidBuildVersionTools"
17+
18+
buildFeatures {
19+
aidl = true
20+
}
21+
22+
defaultConfig {
23+
versionName version
24+
minSdkVersion androidMinSdk
25+
targetSdkVersion androidTargetSdk
26+
}
27+
28+
compileOptions {
29+
sourceCompatibility = 1.8
30+
targetCompatibility = 1.8
31+
}
32+
}
33+
34+
apply from: '../gradle/publish-android.gradle'
35+
36+
description = 'microG implementation of play-services-auth-blockstore'
37+
38+
dependencies {
39+
api project(':play-services-base')
40+
api project(':play-services-basement')
41+
api project(':play-services-tasks')
42+
api 'org.jetbrains.kotlin:kotlin-stdlib:1.9.0'
43+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 microG Project Team
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
apply plugin: 'com.android.library'
7+
apply plugin: 'kotlin-android'
8+
9+
dependencies {
10+
api project(':play-services-auth-blockstore')
11+
implementation project(':play-services-base-core')
12+
}
13+
14+
android {
15+
namespace "org.microg.gms.auth.blockstore"
16+
17+
compileSdkVersion androidCompileSdk
18+
buildToolsVersion "$androidBuildVersionTools"
19+
20+
defaultConfig {
21+
versionName version
22+
minSdkVersion androidMinSdk
23+
targetSdkVersion androidTargetSdk
24+
}
25+
26+
sourceSets {
27+
main.java.srcDirs += 'src/main/kotlin'
28+
}
29+
30+
compileOptions {
31+
sourceCompatibility = 1.8
32+
targetCompatibility = 1.8
33+
}
34+
35+
kotlinOptions {
36+
jvmTarget = 1.8
37+
}
38+
39+
lintOptions {
40+
disable 'MissingTranslation', 'GetLocales'
41+
}
42+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="utf-8"?><!--
2+
~ SPDX-FileCopyrightText: 2025 microG Project Team
3+
~ SPDX-License-Identifier: Apache-2.0
4+
-->
5+
6+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
7+
xmlns:tools="http://schemas.android.com/tools">
8+
9+
<application>
10+
11+
<service android:name="org.microg.gms.auth.blockstore.BlockstoreApiService">
12+
<intent-filter>
13+
<action android:name="com.google.android.gms.auth.blockstore.service.START" />
14+
</intent-filter>
15+
</service>
16+
17+
</application>
18+
</manifest>
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 microG Project Team
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.microg.gms.auth.blockstore
7+
8+
import android.content.Context
9+
import android.content.SharedPreferences
10+
import android.os.Bundle
11+
import android.util.Base64
12+
import android.util.Log
13+
import com.google.android.gms.auth.blockstore.BlockstoreClient
14+
import com.google.android.gms.auth.blockstore.BlockstoreStatusCodes
15+
import com.google.android.gms.auth.blockstore.DeleteBytesRequest
16+
import com.google.android.gms.auth.blockstore.RetrieveBytesRequest
17+
import com.google.android.gms.auth.blockstore.RetrieveBytesResponse
18+
import com.google.android.gms.auth.blockstore.StoreBytesData
19+
import kotlinx.coroutines.Dispatchers
20+
import kotlinx.coroutines.withContext
21+
import org.microg.gms.utils.toBase64
22+
23+
private const val SHARED_PREFS_NAME = "com.google.android.gms.blockstore"
24+
25+
private const val TAG = "BlockStoreImpl"
26+
27+
class BlockStoreImpl(context: Context, val callerPackage: String) {
28+
29+
private val blockStoreSp: SharedPreferences by lazy {
30+
context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE)
31+
}
32+
33+
private fun initSpByPackage(): Map<String, *>? {
34+
val map = blockStoreSp.all
35+
if (map.isNullOrEmpty() || map.all { !it.key.startsWith(callerPackage) }) return null
36+
return map.filter { it.key.startsWith(callerPackage) }
37+
}
38+
39+
suspend fun deleteBytesWithRequest(request: DeleteBytesRequest?): Boolean = withContext(Dispatchers.IO) {
40+
Log.d(TAG, "deleteBytesWithRequest: callerPackage: $callerPackage")
41+
val localData = initSpByPackage()
42+
if (request == null || localData.isNullOrEmpty()) return@withContext false
43+
if (request.deleteAll) {
44+
localData.keys.forEach { blockStoreSp.edit()?.remove(it)?.commit() }
45+
} else {
46+
request.keys.forEach { blockStoreSp.edit()?.remove("$callerPackage:$it")?.commit() }
47+
}
48+
true
49+
}
50+
51+
suspend fun retrieveBytesWithRequest(request: RetrieveBytesRequest?): RetrieveBytesResponse? = withContext(Dispatchers.IO) {
52+
Log.d(TAG, "retrieveBytesWithRequest: callerPackage: $callerPackage")
53+
val localData = initSpByPackage()
54+
if (request == null || localData.isNullOrEmpty()) return@withContext null
55+
val data = mutableListOf<RetrieveBytesResponse.BlockstoreData>()
56+
val filterKeys = if (request.keys.isNullOrEmpty()) emptyList<String>() else request.keys
57+
for (key in localData.keys) {
58+
val bytesKey = key.substring(callerPackage.length + 1)
59+
if (filterKeys.isNotEmpty() && !filterKeys.contains(bytesKey)) continue
60+
val bytes = blockStoreSp.getString(key, null)?.let { Base64.decode(it, Base64.URL_SAFE) } ?: continue
61+
data.add(RetrieveBytesResponse.BlockstoreData(bytes, bytesKey))
62+
}
63+
RetrieveBytesResponse(Bundle.EMPTY, data)
64+
}
65+
66+
suspend fun retrieveBytes(): ByteArray? = withContext(Dispatchers.IO) {
67+
Log.d(TAG, "retrieveBytes: callerPackage: $callerPackage")
68+
val localData = initSpByPackage()
69+
if (localData.isNullOrEmpty()) return@withContext null
70+
val savedKey = localData.keys.firstOrNull { it == "$callerPackage:${BlockstoreClient.DEFAULT_BYTES_DATA_KEY}" } ?: return@withContext null
71+
blockStoreSp.getString(savedKey, null)?.let { Base64.decode(it, Base64.URL_SAFE) }
72+
}
73+
74+
suspend fun storeBytes(data: StoreBytesData?): Int = withContext(Dispatchers.IO) {
75+
if (data == null || data.bytes == null) return@withContext 0
76+
val localData = initSpByPackage()
77+
if ((localData?.size ?: 0) >= BlockstoreClient.MAX_ENTRY_COUNT) {
78+
return@withContext BlockstoreStatusCodes.TOO_MANY_ENTRIES
79+
}
80+
val bytes = data.bytes
81+
if (bytes.size > BlockstoreClient.MAX_SIZE) {
82+
return@withContext BlockstoreStatusCodes.MAX_SIZE_EXCEEDED
83+
}
84+
val savedKey = "$callerPackage:${data.key ?: BlockstoreClient.DEFAULT_BYTES_DATA_KEY}"
85+
val base64 = bytes.toBase64(Base64.URL_SAFE)
86+
val bool = blockStoreSp.edit()?.putString(savedKey, base64)?.commit()
87+
if (bool == true) bytes.size else 0
88+
}
89+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 microG Project Team
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.microg.gms.auth.blockstore
7+
8+
import android.os.Bundle
9+
import android.util.Log
10+
import androidx.lifecycle.Lifecycle
11+
import androidx.lifecycle.LifecycleOwner
12+
import androidx.lifecycle.lifecycleScope
13+
import com.google.android.gms.auth.blockstore.AppRestoreInfo
14+
import com.google.android.gms.auth.blockstore.BlockstoreStatusCodes
15+
import com.google.android.gms.auth.blockstore.DeleteBytesRequest
16+
import com.google.android.gms.auth.blockstore.RetrieveBytesRequest
17+
import com.google.android.gms.auth.blockstore.RetrieveBytesResponse
18+
import com.google.android.gms.auth.blockstore.StoreBytesData
19+
import com.google.android.gms.auth.blockstore.internal.IBlockstoreService
20+
import com.google.android.gms.auth.blockstore.internal.IDeleteBytesCallback
21+
import com.google.android.gms.auth.blockstore.internal.IGetAccessForPackageCallback
22+
import com.google.android.gms.auth.blockstore.internal.IGetBlockstoreDataCallback
23+
import com.google.android.gms.auth.blockstore.internal.IIsEndToEndEncryptionAvailableCallback
24+
import com.google.android.gms.auth.blockstore.internal.IRetrieveBytesCallback
25+
import com.google.android.gms.auth.blockstore.internal.ISetBlockstoreDataCallback
26+
import com.google.android.gms.auth.blockstore.internal.IStoreBytesCallback
27+
import com.google.android.gms.common.Feature
28+
import com.google.android.gms.common.api.CommonStatusCodes
29+
import com.google.android.gms.common.api.Status
30+
import com.google.android.gms.common.api.internal.IStatusCallback
31+
import com.google.android.gms.common.internal.ConnectionInfo
32+
import com.google.android.gms.common.internal.GetServiceRequest
33+
import com.google.android.gms.common.internal.IGmsCallbacks
34+
import kotlinx.coroutines.launch
35+
import org.microg.gms.BaseService
36+
import org.microg.gms.common.GmsService
37+
import org.microg.gms.common.GmsService.BLOCK_STORE
38+
import org.microg.gms.common.PackageUtils
39+
40+
private const val TAG = "BlockstoreApiService"
41+
42+
private val FEATURES = arrayOf(
43+
Feature("auth_blockstore", 3),
44+
Feature("blockstore_data_transfer", 1),
45+
Feature("blockstore_notify_app_restore", 1),
46+
Feature("blockstore_store_bytes_with_options", 2),
47+
Feature("blockstore_is_end_to_end_encryption_available", 1),
48+
Feature("blockstore_enable_cloud_backup", 1),
49+
Feature("blockstore_delete_bytes", 2),
50+
Feature("blockstore_retrieve_bytes_with_options", 3),
51+
Feature("auth_clear_restore_credential", 2),
52+
Feature("auth_create_restore_credential", 1),
53+
Feature("auth_get_restore_credential", 1),
54+
Feature("auth_get_private_restore_credential_key", 1),
55+
Feature("auth_set_private_restore_credential_key", 1),
56+
)
57+
58+
class BlockstoreApiService : BaseService(TAG, BLOCK_STORE) {
59+
60+
override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) {
61+
try {
62+
val packageName = PackageUtils.getAndCheckCallingPackage(this, request.packageName) ?: throw IllegalArgumentException("Missing package name")
63+
64+
val blockStoreImpl = BlockStoreImpl(this, packageName)
65+
callback.onPostInitCompleteWithConnectionInfo(
66+
CommonStatusCodes.SUCCESS, BlobstoreServiceImpl(blockStoreImpl, lifecycle).asBinder(), ConnectionInfo().apply { features = FEATURES })
67+
} catch (e: Exception) {
68+
Log.w(TAG, "handleServiceRequest", e)
69+
callback.onPostInitComplete(CommonStatusCodes.INTERNAL_ERROR, null, null)
70+
}
71+
}
72+
}
73+
74+
class BlobstoreServiceImpl(val blockStore: BlockStoreImpl, override val lifecycle: Lifecycle) : IBlockstoreService.Stub(), LifecycleOwner {
75+
76+
override fun retrieveBytes(callback: IRetrieveBytesCallback?) {
77+
Log.d(TAG, "Method (retrieveBytes) called")
78+
lifecycleScope.launch {
79+
runCatching {
80+
val retrieveBytes = blockStore.retrieveBytes()
81+
if (retrieveBytes != null) {
82+
callback?.onBytesResult(Status.SUCCESS, retrieveBytes)
83+
} else {
84+
callback?.onBytesResult(Status.INTERNAL_ERROR, null)
85+
}
86+
}
87+
}
88+
}
89+
90+
override fun setBlockstoreData(callback: ISetBlockstoreDataCallback?, data: ByteArray?) {
91+
Log.d(TAG, "Method (setBlockstoreData: ${data?.size}) called but not implemented")
92+
}
93+
94+
override fun getBlockstoreData(callback: IGetBlockstoreDataCallback?) {
95+
Log.d(TAG, "Method (getBlockstoreData) called but not implemented")
96+
}
97+
98+
override fun getAccessForPackage(callback: IGetAccessForPackageCallback?, packageName: String?) {
99+
Log.d(TAG, "Method (getAccessForPackage: $packageName) called but not implemented")
100+
}
101+
102+
override fun setFlagWithPackage(callback: IStatusCallback?, packageName: String?, flag: Int) {
103+
Log.d(TAG, "Method (setFlagWithPackage: $packageName, $flag) called but not implemented")
104+
}
105+
106+
override fun clearFlagForPackage(callback: IStatusCallback?, packageName: String?) {
107+
Log.d(TAG, "Method (clearFlagForPackage: $packageName) called but not implemented")
108+
}
109+
110+
override fun updateFlagForPackage(callback: IStatusCallback?, packageName: String?, value: Int) {
111+
Log.d(TAG, "Method (updateFlagForPackage: $packageName, $value) called but not implemented")
112+
}
113+
114+
override fun reportAppRestore(callback: IStatusCallback?, packages: List<String?>?, code: Int, info: AppRestoreInfo?) {
115+
Log.d(TAG, "Method (reportAppRestore: $packages, $code, $info) called but not implemented")
116+
}
117+
118+
override fun storeBytes(callback: IStoreBytesCallback?, data: StoreBytesData?) {
119+
Log.d(TAG, "Method (storeBytes: $data) called")
120+
lifecycleScope.launch {
121+
runCatching {
122+
val storeBytes = blockStore.storeBytes(data)
123+
Log.d(TAG, "storeBytes: size: $storeBytes")
124+
when (storeBytes) {
125+
0 -> callback?.onStoreBytesResult(Status.INTERNAL_ERROR, BlockstoreStatusCodes.FEATURE_NOT_SUPPORTED)
126+
BlockstoreStatusCodes.MAX_SIZE_EXCEEDED -> callback?.onStoreBytesResult(Status.INTERNAL_ERROR, BlockstoreStatusCodes.MAX_SIZE_EXCEEDED)
127+
BlockstoreStatusCodes.TOO_MANY_ENTRIES -> callback?.onStoreBytesResult(Status.INTERNAL_ERROR, BlockstoreStatusCodes.TOO_MANY_ENTRIES)
128+
else -> callback?.onStoreBytesResult(Status.SUCCESS, storeBytes)
129+
}
130+
}
131+
}
132+
}
133+
134+
override fun isEndToEndEncryptionAvailable(callback: IIsEndToEndEncryptionAvailableCallback?) {
135+
Log.d(TAG, "Method (isEndToEndEncryptionAvailable) called")
136+
runCatching { callback?.onCheckEndToEndEncryptionResult(Status.SUCCESS, false) }
137+
}
138+
139+
override fun retrieveBytesWithRequest(callback: IRetrieveBytesCallback?, request: RetrieveBytesRequest?) {
140+
Log.d(TAG, "Method (retrieveBytesWithRequest: $request) called")
141+
lifecycleScope.launch {
142+
runCatching {
143+
val retrieveBytesResponse = blockStore.retrieveBytesWithRequest(request)
144+
Log.d(TAG, "retrieveBytesWithRequest: retrieveBytesResponse: $retrieveBytesResponse")
145+
if (retrieveBytesResponse != null) {
146+
callback?.onResponseResult(Status.SUCCESS, retrieveBytesResponse)
147+
} else {
148+
callback?.onResponseResult(Status.INTERNAL_ERROR, RetrieveBytesResponse(Bundle.EMPTY, emptyList()))
149+
}
150+
}
151+
}
152+
}
153+
154+
override fun deleteBytes(callback: IDeleteBytesCallback?, request: DeleteBytesRequest?) {
155+
Log.d(TAG, "Method (deleteBytes: $request) called")
156+
lifecycleScope.launch {
157+
runCatching {
158+
val deleted = blockStore.deleteBytesWithRequest(request)
159+
callback?.onDeleteBytesResult(Status.SUCCESS, deleted)
160+
}
161+
}
162+
}
163+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
~ SPDX-FileCopyrightText: 2025 microG Project Team
4+
~ SPDX-License-Identifier: Apache-2.0
5+
-->
6+
<manifest />
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 microG Project Team
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package com.google.android.gms.auth.blockstore;
7+
8+
parcelable AppRestoreInfo;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 microG Project Team
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package com.google.android.gms.auth.blockstore;
7+
8+
parcelable DeleteBytesRequest;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 microG Project Team
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package com.google.android.gms.auth.blockstore;
7+
8+
parcelable RetrieveBytesRequest;

0 commit comments

Comments
 (0)