From 35d2b1673d23562dac0eb839e89aa75d9f5eb747 Mon Sep 17 00:00:00 2001 From: skateryna Date: Mon, 27 Jul 2026 18:35:50 -0700 Subject: [PATCH 1/2] Remove location permission --- agent/app/src/main/AndroidManifest.xml | 22 +- .../appfunctions/agent/MainActivity.kt | 13 - .../data/BaseBuiltInAppFunctionService.kt | 360 ------------------ agent/app/src/main/res/xml/app_metadata.xml | 7 +- 4 files changed, 2 insertions(+), 400 deletions(-) delete mode 100644 agent/app/src/main/java/com/example/appfunctions/agent/data/BaseBuiltInAppFunctionService.kt diff --git a/agent/app/src/main/AndroidManifest.xml b/agent/app/src/main/AndroidManifest.xml index 699a2e4..c8dce3e 100644 --- a/agent/app/src/main/AndroidManifest.xml +++ b/agent/app/src/main/AndroidManifest.xml @@ -16,15 +16,10 @@ --> - - - - - - - - - - - - \ No newline at end of file + diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/MainActivity.kt b/agent/app/src/main/java/com/example/appfunctions/agent/MainActivity.kt index 6b4a6a8..2bb58ce 100644 --- a/agent/app/src/main/java/com/example/appfunctions/agent/MainActivity.kt +++ b/agent/app/src/main/java/com/example/appfunctions/agent/MainActivity.kt @@ -15,12 +15,10 @@ */ package com.example.appfunctions.agent -import android.Manifest import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -68,20 +66,9 @@ import dagger.hilt.android.AndroidEntryPoint */ @AndroidEntryPoint class MainActivity : ComponentActivity() { - private val requestPermissionLauncher = - registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { _ -> } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (BuildConfig.IS_RETAIL) { - // Only requesting location permission onStart when running for demo purpose - requestPermissionLauncher.launch( - arrayOf( - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.ACCESS_COARSE_LOCATION, - ), - ) - } enableEdgeToEdge() setContent { AppFunctionsAgentTheme { diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/data/BaseBuiltInAppFunctionService.kt b/agent/app/src/main/java/com/example/appfunctions/agent/data/BaseBuiltInAppFunctionService.kt deleted file mode 100644 index 2e1eb54..0000000 --- a/agent/app/src/main/java/com/example/appfunctions/agent/data/BaseBuiltInAppFunctionService.kt +++ /dev/null @@ -1,360 +0,0 @@ -/* - * Copyright 2026 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 - * - * https://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.example.appfunctions.agent.data - -import android.Manifest -import android.annotation.SuppressLint -import android.content.Context -import android.content.pm.PackageManager -import android.location.Address -import android.location.Geocoder -import android.location.LocationManager -import android.util.Base64 -import androidx.annotation.RequiresApi -import androidx.appfunctions.AppFunction -import androidx.appfunctions.AppFunctionSerializable -import androidx.appfunctions.AppFunctionService -import androidx.appfunctions.AppFunctionServiceEntryPoint -import androidx.core.content.ContextCompat -import androidx.core.content.FileProvider -import androidx.datastore.preferences.core.stringPreferencesKey -import com.example.appfunctions.agent.BuildConfig -import com.example.appfunctions.agent.di.settingsDataStore -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.withContext -import org.json.JSONArray -import org.json.JSONObject -import java.io.File -import java.net.HttpURLConnection -import java.net.URL -import java.util.UUID -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -/** Built-in AppFunctions for location and geocoding services. */ -@RequiresApi(36) -@AndroidEntryPoint -@AppFunctionServiceEntryPoint( - serviceName = "BuiltInAppFunctionService", - appFunctionXmlFileName = "builtin_app_function_service", -) -abstract class BaseBuiltInAppFunctionService : AppFunctionService() { - /** - * Geocode a physical address string into its latitude and longitude coordinates. - * - * @param address The physical address to geocode (e.g., "1600 Amphitheatre Pkwy, Mountain View, - * CA"). - * @return The latitude and longitude coordinates of the address, or null if geocoding fails. - */ - @AppFunction(isDescribedByKDoc = true) - suspend fun geocodeAddress(address: String): LatLng? { - if (!Geocoder.isPresent()) { - return null - } - - val geocoder = Geocoder(this) - - return withContext(Dispatchers.IO) { - try { - suspendCoroutine { continuation -> - geocoder.getFromLocationName( - address, - 1, - object : Geocoder.GeocodeListener { - override fun onGeocode(addresses: MutableList
) { - val location = addresses.firstOrNull() - if (location != null) { - continuation.resume( - LatLng(location.latitude, location.longitude), - ) - } else { - continuation.resume(null) - } - } - - override fun onError(errorMessage: String?) { - continuation.resume(null) - } - }, - ) - } - } catch (e: Exception) { - throw IllegalStateException(e.message, e) - } - } - } - - /** - * Retrieve the current latitude and longitude coordinates of the device. - * - * @return The current location coordinates of the device, or null if location is unavailable or - * permission is denied. - */ - @SuppressLint("MissingPermission") - @AppFunction(isDescribedByKDoc = true) - suspend fun getCurrentLocation(): LatLng? = - withContext(Dispatchers.Default) { - // Check permissions - val hasFineLocation = - ContextCompat.checkSelfPermission( - this@BaseBuiltInAppFunctionService, - Manifest.permission.ACCESS_FINE_LOCATION, - ) == PackageManager.PERMISSION_GRANTED - - val hasCoarseLocation = - ContextCompat.checkSelfPermission( - this@BaseBuiltInAppFunctionService, - Manifest.permission.ACCESS_COARSE_LOCATION, - ) == PackageManager.PERMISSION_GRANTED - - if (!hasFineLocation && !hasCoarseLocation) { - throw IllegalStateException("Location permission is not granted") - } - - val locationManager = - this@BaseBuiltInAppFunctionService.getSystemService(Context.LOCATION_SERVICE) as LocationManager - - try { - // Try GPS Provider first - var location = - if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { - locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) - } else { - null - } - - // Fallback to Network Provider if GPS is not available - if (location == null && - locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) - ) { - location = - locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER) - } - - if (location != null) { - LatLng(location.latitude, location.longitude) - } else { - null - } - } catch (e: Exception) { - throw IllegalStateException(e.message, e) - } - } - - /** Represents the latitude and longitude coordinates. */ - @AppFunctionSerializable(isDescribedByKDoc = true) - data class LatLng( - /** The latitude coordinate. */ - val latitude: Double, - /** The longitude coordinate. */ - val longitude: Double, - ) - - /** - * Generates an image from a text prompt and returns the remote image URI. - * - * @param prompt The text prompt describing the image to generate (e.g., "futuristic cityscape at sunset"). - * @param aspectRatio Optional aspect ratio for the image (e.g., "16:9", "1:1"). - * @return A GeneratedImageResult containing the generated remote image URI. - */ - @AppFunction(isDescribedByKDoc = true) - suspend fun generateImage( - prompt: String, - aspectRatio: String? = null, - ): GeneratedImageResult = - withContext(Dispatchers.IO) { - val apiKey = getOrFetchApiKey() - val requestPayload = buildImageGenerationPayload(prompt, aspectRatio) - val responseText = executeImageRequest(apiKey, requestPayload) - saveBase64ImageToCache(responseText, prompt) - } - - private suspend fun getOrFetchApiKey(): String { - val apiKey = - settingsDataStore.data - .first()[stringPreferencesKey("gemini_api_key")] - ?.takeIf { it.isNotBlank() } - ?: BuildConfig.GEMINI_API_KEY.takeIf { it.isNotBlank() } - if (apiKey.isNullOrBlank()) { - throw IllegalStateException( - "Gemini API key is not configured. Please set gemini_api_key in settings.", - ) - } - return apiKey - } - - private fun buildImageGenerationPayload( - prompt: String, - aspectRatio: String?, - ): JSONObject = - JSONObject().apply { - put( - "contents", - JSONArray().apply { - put( - JSONObject().apply { - put( - "parts", - JSONArray().apply { - put(JSONObject().apply { put("text", prompt) }) - }, - ) - }, - ) - }, - ) - put( - "generationConfig", - JSONObject().apply { - put("responseModalities", JSONArray().apply { put("IMAGE") }) - if (!aspectRatio.isNullOrBlank()) { - put( - "imageConfig", - JSONObject().apply { - put("aspectRatio", aspectRatio) - }, - ) - } - }, - ) - } - - private fun executeImageRequest( - apiKey: String, - requestJson: JSONObject, - ): String { - val endpointUrl = - "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-flash-image-preview:generateContent?key=$apiKey" - val url = URL(endpointUrl) - val connection = - (url.openConnection() as HttpURLConnection).apply { - requestMethod = "POST" - setRequestProperty("Content-Type", "application/json") - doOutput = true - connectTimeout = 30000 - readTimeout = 60000 - } - - try { - connection.outputStream.use { os -> - os.write(requestJson.toString().toByteArray(Charsets.UTF_8)) - } - - val responseCode = connection.responseCode - if (responseCode != HttpURLConnection.HTTP_OK) { - val errorBody = - connection.errorStream?.bufferedReader()?.use { it.readText() } - ?: "HTTP $responseCode" - throw IllegalStateException( - "Image generation failed ($responseCode): $errorBody", - ) - } - - return connection.inputStream.bufferedReader().use { it.readText() } - } finally { - connection.disconnect() - } - } - - private fun saveBase64ImageToCache( - responseText: String, - prompt: String, - ): GeneratedImageResult { - val responseJson = JSONObject(responseText) - val candidates = responseJson.optJSONArray("candidates") - if (candidates == null || candidates.length() == 0) { - throw IllegalStateException( - "No candidates returned from Gemini image generation API", - ) - } - - val parts = - candidates - .getJSONObject(0) - .optJSONObject("content") - ?.optJSONArray("parts") - if (parts == null || parts.length() == 0) { - throw IllegalStateException( - "No parts returned in candidate content. Gemini response: $responseText", - ) - } - - val candidateData = - (0 until parts.length()) - .asSequence() - .mapNotNull { i -> - val part = parts.getJSONObject(i) - val inlineData = - part.optJSONObject("inlineData") - ?: part.optJSONObject("inline_data") - if (inlineData != null) { - val base64 = inlineData.optString("data") - val returnedMime = - inlineData - .optString("mimeType") - .takeIf { it.isNotBlank() } - ?: inlineData.optString("mime_type") - .takeIf { it.isNotBlank() } - ?: "image/png" - base64 to returnedMime - } else { - null - } - } - .firstOrNull() - - if (candidateData == null || candidateData.first.isBlank()) { - throw IllegalStateException( - "No inlineData image found in response parts. Gemini response: $responseText", - ) - } - - val (base64Data, mimeType) = candidateData - val imageBytes = Base64.decode(base64Data, Base64.DEFAULT) - val extension = - when { - mimeType.contains("jpeg") || mimeType.contains("jpg") -> "jpg" - else -> "png" - } - val cachedFile = - File( - cacheDir, - "generated_${UUID.randomUUID()}.$extension", - ) - cachedFile.writeBytes(imageBytes) - - val authority = "$packageName.fileprovider" - val contentUri = FileProvider.getUriForFile(this, authority, cachedFile) - return GeneratedImageResult( - imageUri = contentUri.toString(), - mimeType = mimeType, - prompt = prompt, - ) - } - - /** Represents the result of an image generation request. */ - @AppFunctionSerializable - data class GeneratedImageResult( - /** The remote URI or URL of the generated image. */ - val imageUri: String, - /** The MIME type of the generated image. */ - val mimeType: String, - /** The original prompt used to generate the image. */ - val prompt: String, - ) -} diff --git a/agent/app/src/main/res/xml/app_metadata.xml b/agent/app/src/main/res/xml/app_metadata.xml index 81cf42b..6e605d2 100644 --- a/agent/app/src/main/res/xml/app_metadata.xml +++ b/agent/app/src/main/res/xml/app_metadata.xml @@ -15,10 +15,5 @@ ~ limitations under the License. --> From d97595f4777361dcf0f1c30b9ef6d772d07452e8 Mon Sep 17 00:00:00 2001 From: skateryna Date: Mon, 27 Jul 2026 19:59:27 -0700 Subject: [PATCH 2/2] spotlessApply --- .../src/main/java/com/example/appfunctions/agent/MainActivity.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/MainActivity.kt b/agent/app/src/main/java/com/example/appfunctions/agent/MainActivity.kt index 2bb58ce..818ec29 100644 --- a/agent/app/src/main/java/com/example/appfunctions/agent/MainActivity.kt +++ b/agent/app/src/main/java/com/example/appfunctions/agent/MainActivity.kt @@ -66,7 +66,6 @@ import dagger.hilt.android.AndroidEntryPoint */ @AndroidEntryPoint class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge()