Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit e29b5249 authored by DaVinci9196's avatar DaVinci9196 Committed by GitHub
Browse files

Blockstore: Add local-only implementation (#3036)



Co-authored-by: default avatarMarvin W <git@larma.de>
parent 9c375660
Loading
Loading
Loading
Loading
+43 −0
Original line number Diff line number Diff line
/*
 * 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'
}
+42 −0
Original line number Diff line number Diff line
/*
 * 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'
    }
}
+18 −0
Original line number Diff line number Diff line
<?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>
+89 −0
Original line number Diff line number Diff line
/*
 * 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
    }
}
 No newline at end of file
+163 −0
Original line number Diff line number Diff line
/*
 * 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)
            }
        }
    }
}
 No newline at end of file
Loading