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

Commit 2830e83d authored by Yi Jiang's avatar Yi Jiang Committed by Android (Google) Code Review
Browse files

Merge "Device State LLM infrastructure" into main

parents 9699e134 e46b34a0
Loading
Loading
Loading
Loading
+12 −0
Original line number Diff line number Diff line
@@ -65,6 +65,7 @@ android_library {
        "res-export", // for external usage
        "res-product",
    ],
    optional_uses_libs: ["com.android.extensions.appfunctions"],
    static_libs: [
        // External dependencies
        "androidx.navigation_navigation-fragment-ktx",
@@ -116,6 +117,12 @@ android_library {
        "keyboard_flags_lib",
        "com_android_systemui_flags_lib",
        "settings_connectivity_flags_lib",

        // app function dependencies
        "androidx.appsearch_appsearch",
        "androidx.appsearch_appsearch-builtin-types",
        "com.android.extensions.appfunctions.impl",
        "device-state-schema",
    ],

    plugins: [
@@ -185,6 +192,11 @@ android_library_import {
    aars: ["libs/contextualcards.aar"],
}

android_library_import {
    name: "device-state-schema",
    aars: ["libs/appfunctions_schema_androidx_archive.aar"],
}

filegroup {
    name: "Settings_proguard_flags",
    srcs: ["proguard.flags"],
+13 −0
Original line number Diff line number Diff line
@@ -170,6 +170,7 @@

        <uses-library android:name="org.apache.http.legacy" />


        <property
            android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"
            android:value="true" />
@@ -5666,6 +5667,18 @@
                android:value="@string/menu_key_system"/>
        </activity>

        <service
            android:name=".appfunctions.DeviceStateAppFunctionService"
            android:permission="android.permission.BIND_APP_FUNCTION_SERVICE"
            android:exported="true"
            android:featureFlag="com.android.settings.flags.device_state">
            <property android:name="android.app.appfunctions"
                android:value="appfunctions.xml"/>
            <intent-filter>
                <action android:name="android.app.appfunctions.AppFunctionService"/>
            </intent-filter>
        </service>

        <!-- This is the longest AndroidManifest.xml ever. -->
    </application>
</manifest>
+54 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<!--
  ~ Copyright (C) 2025 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.
  -->

<appfunctions>
    <appfunction>
        <function_id>getUncategorizedDeviceState</function_id>
        <schema_name>getUncategorizedDeviceState</schema_name>
        <schema_category>device_state</schema_category>
        <schema_version>1</schema_version>
        <enabled_by_default>true</enabled_by_default>
    </appfunction>
    <appfunction>
        <function_id>getStorageDeviceState</function_id>
        <schema_name>getStorageDeviceState</schema_name>
        <schema_category>device_state</schema_category>
        <schema_version>1</schema_version>
        <enabled_by_default>true</enabled_by_default>
    </appfunction>
    <appfunction>
        <function_id>getBatteryDeviceState</function_id>
        <schema_name>getBatteryDeviceState</schema_name>
        <schema_category>device_state</schema_category>
        <schema_version>1</schema_version>
        <enabled_by_default>true</enabled_by_default>
    </appfunction>
    <appfunction>
        <function_id>getMobileDataUsageDeviceState</function_id>
        <schema_name>getMobileDataUsageDeviceState</schema_name>
        <schema_category>device_state</schema_category>
        <schema_version>1</schema_version>
        <enabled_by_default>true</enabled_by_default>
    </appfunction>
    <appfunction>
        <function_id>getPermissionsDeviceState</function_id>
        <schema_name>getPermissionsDeviceState</schema_name>
        <schema_category>device_state</schema_category>
        <schema_version>1</schema_version>
        <enabled_by_default>true</enabled_by_default>
    </appfunction>
</appfunctions>
+14.1 KiB

File added.

No diff preview for this file type.

+209 −0
Original line number Diff line number Diff line
/*
 * Copyright 2025 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 com.android.settings.appfunctions

import android.app.appsearch.GenericDocument
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.content.res.Resources
import android.os.CancellationSignal
import android.os.OutcomeReceiver
import android.util.Log
import com.android.extensions.appfunctions.AppFunctionException
import com.android.extensions.appfunctions.AppFunctionException.ERROR_FUNCTION_NOT_FOUND
import com.android.extensions.appfunctions.AppFunctionService
import com.android.extensions.appfunctions.ExecuteAppFunctionRequest
import com.android.extensions.appfunctions.ExecuteAppFunctionResponse
import com.android.settings.utils.getLocale
import com.android.settingslib.metadata.PersistentPreference
import com.android.settingslib.metadata.PreferenceScreenCoordinate
import com.android.settingslib.metadata.PreferenceScreenRegistry
import com.google.android.appfunctions.schema.common.v1.devicestate.DeviceStateItem
import com.google.android.appfunctions.schema.common.v1.devicestate.DeviceStateResponse
import com.google.android.appfunctions.schema.common.v1.devicestate.LocalizedString
import com.google.android.appfunctions.schema.common.v1.devicestate.PerScreenDeviceStates
import java.util.Locale
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking

class DeviceStateAppFunctionService : AppFunctionService() {
    private val settingConfigMap = getDeviceStateItemList().associateBy { it.settingKey }
    private val perScreenConfigMap = getScreenConfigs().associateBy { it.screenKey }
    private lateinit var englishContext: Context

    override fun onCreate() {
        super.onCreate()
        englishContext = createEnglishContext()
    }

    override fun onExecuteFunction(
        request: ExecuteAppFunctionRequest,
        callingPackage: String, cancellationSignal: CancellationSignal,
        callback: OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException>
    ) {
        val requestCategory = DeviceStateCategory.fromId(request.functionIdentifier)
        if (requestCategory == null) {
            callback.onError(
                AppFunctionException(
                    ERROR_FUNCTION_NOT_FOUND,
                    "${request.functionIdentifier} not supported."
                )
            )
            return
        }
        runBlocking {
            Log.d(TAG, "device state app function ${request.functionIdentifier} called.")
            val jetpackDocument =
                androidx.appsearch.app.GenericDocument.fromDocumentClass(
                    buildResponseFromCatalyst(
                        requestCategory
                    )
                )
            val platformDocument =
                GenericDocumentToPlatformConverter.toPlatformGenericDocument(jetpackDocument)
            val result =
                GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "")
                    .setPropertyDocument(
                        ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE,
                        platformDocument
                    )
                    .build()
            val response = ExecuteAppFunctionResponse(result)
            callback.onResult(response)
            Log.d(TAG, "app function ${request.functionIdentifier} fulfilled.")
        }
    }

    private suspend fun buildResponseFromCatalyst(
        requestCategory: DeviceStateCategory
    ): DeviceStateResponse {
        val screenKeyList = perScreenConfigMap.keys.toList()
        val perScreenDeviceStatesList: MutableList<PerScreenDeviceStates> = ArrayList()
        coroutineScope {
            val deferredList = screenKeyList.map { screenKey ->
                async { buildPerScreenDeviceStates(screenKey, requestCategory) }
            }
            deferredList.awaitAll().forEach {
                if (it != null) {
                    perScreenDeviceStatesList.add(it)
                }
            }
        }
        return DeviceStateResponse(
            perScreenDeviceStates = perScreenDeviceStatesList,
            deviceLocale = applicationContext.getLocale().toString()
        )
    }

    private fun buildPerScreenDeviceStates(
        screenKey: String,
        requestCategory: DeviceStateCategory,
    ): PerScreenDeviceStates? {
        val perScreenConfig = perScreenConfigMap[screenKey]
        if (perScreenConfig == null || !perScreenConfig.enabled || requestCategory !in perScreenConfig.category) {
            return null
        }
        val screenMetaData =
            PreferenceScreenRegistry.create(
                applicationContext,
                PreferenceScreenCoordinate(screenKey, null)
            )
        if (screenMetaData == null) {
            return null
        }
        val deviceStateItemList: MutableList<DeviceStateItem> = ArrayList()
        // TODO(b/405344827): support PreferenceHierarchyGenerator
        val hierarchy = screenMetaData.getPreferenceHierarchy(applicationContext)
        hierarchy.forEach {
            val metadata = it.metadata as? PersistentPreference<*> ?: return@forEach
            val config = settingConfigMap[metadata.key]
            if (config == null || !config.enabled) {
                return@forEach
            }
            val valueType = metadata.valueType
            var jasonValue: String? = when (valueType) {
                Int::class.javaObjectType -> metadata.storage(applicationContext)
                    ?.getInt("")
                    .toString()

                Boolean::class.javaObjectType -> metadata.storage(applicationContext)
                    ?.getBoolean("").toString()

                Long::class.javaObjectType -> metadata.storage(applicationContext)
                    ?.getLong("")
                    .toString()

                Float::class.javaObjectType -> metadata.storage(applicationContext)
                    ?.getLong("")
                    .toString()

                String::class.javaObjectType -> metadata.storage(applicationContext)?.getString("")
                else -> null
            }
            if (jasonValue == null) {
                jasonValue = tryGetStringRes(metadata.summary)
            }
            deviceStateItemList.add(
                DeviceStateItem(
                    key = metadata.key,
                    name = getLocalizedString(metadata.title),
                    jsonValue = jasonValue,
                    hintText = config.hintText
                )
            )
        }

        val launchingIntent = screenMetaData.getLaunchIntent(applicationContext, null)
        return PerScreenDeviceStates(
            description = tryGetStringRes(screenMetaData.title),
            deviceStateItems = deviceStateItemList,
            intentUri = launchingIntent?.toUri(Intent.URI_INTENT_SCHEME)
        )
    }

    private fun tryGetStringRes(resId: Int): String {
        return try {
            applicationContext.getString(resId)
        } catch (_: Resources.NotFoundException) {
            ""
        }
    }

    private fun getLocalizedString(resId: Int): LocalizedString? {
        return try {
            LocalizedString(
                english = englishContext.getString(resId),
                localized = applicationContext.getString(resId)
            )
        } catch (_: Resources.NotFoundException) {
            null
        }
    }

    private fun createEnglishContext(): Context {
        val configuration = Configuration(applicationContext.resources.configuration)
        configuration.setLocale(Locale.US)
        return applicationContext.createConfigurationContext(configuration)
    }

    companion object {
        private const val TAG = "DeviceStateService"
    }
}
Loading