LocalSdkRegistry.kt

/*
 * Copyright 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.privacysandbox.sdkruntime.client.controller.impl

import android.content.Context
import android.os.Bundle
import android.util.Log
import androidx.privacysandbox.sdkruntime.client.activity.LocalSdkActivityHandlerRegistry
import androidx.privacysandbox.sdkruntime.client.config.LocalSdkConfigsHolder
import androidx.privacysandbox.sdkruntime.client.controller.AppOwnedSdkRegistry
import androidx.privacysandbox.sdkruntime.client.controller.LocalControllerFactory
import androidx.privacysandbox.sdkruntime.client.controller.SdkRegistry
import androidx.privacysandbox.sdkruntime.client.loader.LocalSdkProvider
import androidx.privacysandbox.sdkruntime.client.loader.SdkLoader
import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException
import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
import org.jetbrains.annotations.TestOnly

/**
 * Responsible for lifecycle of SDKs bundled with app.
 * Shared between:
 * 1) [androidx.privacysandbox.sdkruntime.client.SdkSandboxManagerCompat]
 * 2) [androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat]
 */
internal class LocalSdkRegistry(
    private val configHolder: LocalSdkConfigsHolder,
) : SdkRegistry {
    private lateinit var sdkLoader: SdkLoader

    private val sdks = HashMap<String, Entry>()

    override fun isResponsibleFor(sdkName: String): Boolean {
        return configHolder.getSdkConfig(sdkName) != null
    }

    override fun loadSdk(sdkName: String, params: Bundle): SandboxedSdkCompat {
        val sdkConfig = configHolder.getSdkConfig(sdkName)
        if (sdkConfig == null) {
            throw LoadSdkCompatException(
                LoadSdkCompatException.LOAD_SDK_NOT_FOUND,
                "$sdkName not bundled with app"
            )
        }

        synchronized(sdks) {
            if (sdks.containsKey(sdkName)) {
                throw LoadSdkCompatException(
                    LoadSdkCompatException.LOAD_SDK_ALREADY_LOADED,
                    "$sdkName already loaded"
                )
            }

            val sdkProvider = sdkLoader.loadSdk(sdkConfig)
            val sandboxedSdkCompat = sdkProvider.onLoadSdk(params)
            sdks.put(
                sdkName, Entry(
                    sdkProvider = sdkProvider,
                    sdk = sandboxedSdkCompat
                )
            )
            return sandboxedSdkCompat
        }
    }

    override fun unloadSdk(sdkName: String) {
        val loadedEntry = synchronized(sdks) {
            sdks.remove(sdkName)
        }
        if (loadedEntry == null) {
            Log.w(LOG_TAG, "Unloading SDK that is not loaded - $sdkName")
            return
        }

        loadedEntry.sdkProvider.beforeUnloadSdk()
        LocalSdkActivityHandlerRegistry.unregisterAllActivityHandlersForSdk(sdkName)
    }

    override fun getLoadedSdks(): List<SandboxedSdkCompat> = synchronized(sdks) {
        return sdks.values.map { it.sdk }
    }

    @TestOnly
    fun getLoadedSdkProvider(sdkName: String): LocalSdkProvider? = synchronized(sdks) {
        return sdks[sdkName]?.sdkProvider
    }

    private data class Entry(
        val sdkProvider: LocalSdkProvider,
        val sdk: SandboxedSdkCompat
    )

    companion object {
        const val LOG_TAG = "LocalSdkRegistry"

        /**
         * Create and initialize all required components for loading SDKs bundled with app.
         *
         * @param context App context
         * @param appOwnedSdkRegistry AppOwnedSdkRegistry for [LocalControllerFactory]
         * @return LocalSdkRegistry that could load SDKs bundled with app.
         */
        fun create(
            context: Context,
            appOwnedSdkRegistry: AppOwnedSdkRegistry
        ): LocalSdkRegistry {
            val configHolder = LocalSdkConfigsHolder.load(context)

            val localSdkRegistry = LocalSdkRegistry(configHolder)
            localSdkRegistry.sdkLoader = SdkLoader.create(
                context,
                LocalControllerFactory(
                    localSdkRegistry,
                    appOwnedSdkRegistry
                )
            )

            return localSdkRegistry
        }
    }
}