1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.app.appfunctions; 18 19 import static android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_DEFAULT; 20 import static android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_ENABLED; 21 import static android.app.appfunctions.AppFunctionRuntimeMetadata.PROPERTY_APP_FUNCTION_STATIC_METADATA_QUALIFIED_ID; 22 import static android.app.appfunctions.AppFunctionRuntimeMetadata.PROPERTY_ENABLED; 23 import static android.app.appfunctions.AppFunctionStaticMetadataHelper.APP_FUNCTION_INDEXER_PACKAGE; 24 import static android.app.appfunctions.AppFunctionStaticMetadataHelper.STATIC_PROPERTY_ENABLED_BY_DEFAULT; 25 import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER; 26 27 import android.Manifest; 28 import android.annotation.FlaggedApi; 29 import android.annotation.NonNull; 30 import android.app.appsearch.AppSearchManager; 31 import android.app.appsearch.AppSearchResult; 32 import android.app.appsearch.GlobalSearchSession; 33 import android.app.appsearch.JoinSpec; 34 import android.app.appsearch.PropertyPath; 35 import android.app.appsearch.SearchResult; 36 import android.app.appsearch.SearchResults; 37 import android.app.appsearch.SearchSpec; 38 import android.os.OutcomeReceiver; 39 import android.text.TextUtils; 40 41 import java.io.IOException; 42 import java.util.List; 43 import java.util.Objects; 44 import java.util.concurrent.Executor; 45 46 /** 47 * Helper class containing utilities for {@link AppFunctionManager}. 48 * 49 * @hide 50 */ 51 @FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER) 52 public class AppFunctionManagerHelper { 53 54 /** 55 * Returns (through a callback) a boolean indicating whether the app function is enabled. 56 * 57 * This method can only check app functions owned by the caller, or those where the caller 58 * has visibility to the owner package and holds the {@link 59 * Manifest.permission#EXECUTE_APP_FUNCTIONS} permission. 60 * 61 * <p>If operation fails, the callback's {@link OutcomeReceiver#onError} is called with errors: 62 * 63 * <ul> 64 * <li>{@link AppFunctionNotFoundException}, if the function is not found or the caller does 65 * not have access to it. 66 * </ul> 67 * 68 * @param functionIdentifier the identifier of the app function to check (unique within the 69 * target package) and in most cases, these are automatically 70 * generated by the AppFunctions 71 * SDK 72 * @param targetPackage the package name of the app function's owner 73 * @param executor executor the executor to run the request 74 * @param callback the callback to receive the function enabled check result 75 * @hide 76 */ isAppFunctionEnabled( @onNull String functionIdentifier, @NonNull String targetPackage, @NonNull AppSearchManager appSearchManager, @NonNull Executor executor, @NonNull OutcomeReceiver<Boolean, Exception> callback)77 public static void isAppFunctionEnabled( 78 @NonNull String functionIdentifier, 79 @NonNull String targetPackage, 80 @NonNull AppSearchManager appSearchManager, 81 @NonNull Executor executor, 82 @NonNull OutcomeReceiver<Boolean, Exception> callback) { 83 Objects.requireNonNull(functionIdentifier); 84 Objects.requireNonNull(targetPackage); 85 Objects.requireNonNull(appSearchManager); 86 Objects.requireNonNull(executor); 87 Objects.requireNonNull(callback); 88 89 appSearchManager.createGlobalSearchSession( 90 executor, 91 (searchSessionResult) -> { 92 if (!searchSessionResult.isSuccess()) { 93 callback.onError(failedResultToException(searchSessionResult)); 94 return; 95 } 96 try (GlobalSearchSession searchSession = searchSessionResult.getResultValue()) { 97 SearchResults results = 98 searchJoinedStaticWithRuntimeAppFunctions( 99 Objects.requireNonNull(searchSession), 100 targetPackage, 101 functionIdentifier); 102 results.getNextPage( 103 executor, 104 listAppSearchResult -> { 105 if (listAppSearchResult.isSuccess()) { 106 callback.onResult( 107 getEffectiveEnabledStateFromSearchResults( 108 Objects.requireNonNull( 109 listAppSearchResult 110 .getResultValue()))); 111 } else { 112 callback.onError( 113 failedResultToException(listAppSearchResult)); 114 } 115 }); 116 results.close(); 117 } catch (Exception e) { 118 callback.onError(e); 119 } 120 }); 121 } 122 123 /** 124 * Searches joined app function static and runtime metadata using the function Id and the 125 * package. 126 */ searchJoinedStaticWithRuntimeAppFunctions( @onNull GlobalSearchSession session, @NonNull String targetPackage, @NonNull String functionIdentifier)127 private static @NonNull SearchResults searchJoinedStaticWithRuntimeAppFunctions( 128 @NonNull GlobalSearchSession session, 129 @NonNull String targetPackage, 130 @NonNull String functionIdentifier) { 131 SearchSpec runtimeSearchSpec = 132 getAppFunctionRuntimeMetadataSearchSpecByPackageName(targetPackage); 133 JoinSpec joinSpec = 134 new JoinSpec.Builder(PROPERTY_APP_FUNCTION_STATIC_METADATA_QUALIFIED_ID) 135 .setNestedSearch( 136 buildFilerRuntimeMetadataByFunctionIdQuery(functionIdentifier), 137 runtimeSearchSpec) 138 .build(); 139 SearchSpec joinedStaticWithRuntimeSearchSpec = 140 new SearchSpec.Builder() 141 .addFilterPackageNames(APP_FUNCTION_INDEXER_PACKAGE) 142 .addFilterSchemas( 143 AppFunctionStaticMetadataHelper.getStaticSchemaNameForPackage( 144 targetPackage)) 145 .addProjectionPaths( 146 SearchSpec.SCHEMA_TYPE_WILDCARD, 147 List.of(new PropertyPath(STATIC_PROPERTY_ENABLED_BY_DEFAULT))) 148 .setJoinSpec(joinSpec) 149 .setVerbatimSearchEnabled(true) 150 .build(); 151 return session.search( 152 buildFilerStaticMetadataByFunctionIdQuery(functionIdentifier), 153 joinedStaticWithRuntimeSearchSpec); 154 } 155 156 /** 157 * Returns whether the function is effectively enabled or not from the search results returned 158 * by {@link #searchJoinedStaticWithRuntimeAppFunctions}. 159 * 160 * @param joinedStaticRuntimeResults search results joining AppFunctionStaticMetadata 161 * and AppFunctionRuntimeMetadata. 162 * @throws IllegalArgumentException if the function is not found in the results 163 */ getEffectiveEnabledStateFromSearchResults( @onNull List<SearchResult> joinedStaticRuntimeResults)164 private static boolean getEffectiveEnabledStateFromSearchResults( 165 @NonNull List<SearchResult> joinedStaticRuntimeResults) { 166 if (joinedStaticRuntimeResults.isEmpty()) { 167 throw new IllegalArgumentException("App function not found."); 168 } else { 169 List<SearchResult> runtimeMetadataResults = 170 joinedStaticRuntimeResults.getFirst().getJoinedResults(); 171 if (runtimeMetadataResults.isEmpty()) { 172 throw new IllegalArgumentException("App function not found."); 173 } 174 long enabled = 175 runtimeMetadataResults 176 .getFirst() 177 .getGenericDocument() 178 .getPropertyLong(PROPERTY_ENABLED); 179 // If enabled is not equal to APP_FUNCTION_STATE_DEFAULT, it means it IS overridden and 180 // we should return the overridden value. 181 if (enabled != APP_FUNCTION_STATE_DEFAULT) { 182 return enabled == APP_FUNCTION_STATE_ENABLED; 183 } 184 // Runtime metadata not found or enabled is equal to APP_FUNCTION_STATE_DEFAULT. 185 // Using the default value in the static metadata. 186 return joinedStaticRuntimeResults 187 .getFirst() 188 .getGenericDocument() 189 .getPropertyBoolean(STATIC_PROPERTY_ENABLED_BY_DEFAULT); 190 } 191 } 192 193 /** 194 * Returns search spec that queries app function metadata for a specific package name by its 195 * function identifier. 196 */ getAppFunctionRuntimeMetadataSearchSpecByPackageName( @onNull String targetPackage)197 private static @NonNull SearchSpec getAppFunctionRuntimeMetadataSearchSpecByPackageName( 198 @NonNull String targetPackage) { 199 return new SearchSpec.Builder() 200 .addFilterPackageNames(APP_FUNCTION_INDEXER_PACKAGE) 201 .addFilterSchemas( 202 AppFunctionRuntimeMetadata.getRuntimeSchemaNameForPackage(targetPackage)) 203 .setVerbatimSearchEnabled(true) 204 .build(); 205 } 206 buildFilerRuntimeMetadataByFunctionIdQuery(String functionIdentifier)207 private static String buildFilerRuntimeMetadataByFunctionIdQuery(String functionIdentifier) { 208 return TextUtils.formatSimple("%s:\"%s\"", 209 AppFunctionRuntimeMetadata.PROPERTY_FUNCTION_ID, 210 functionIdentifier); 211 } 212 buildFilerStaticMetadataByFunctionIdQuery(String functionIdentifier)213 private static String buildFilerStaticMetadataByFunctionIdQuery(String functionIdentifier) { 214 return TextUtils.formatSimple("%s:\"%s\"", 215 AppFunctionStaticMetadataHelper.PROPERTY_FUNCTION_ID, 216 functionIdentifier); 217 } 218 219 /** Converts a failed app search result codes into an exception. */ failedResultToException( @onNull AppSearchResult appSearchResult)220 private static @NonNull Exception failedResultToException( 221 @NonNull AppSearchResult appSearchResult) { 222 return switch (appSearchResult.getResultCode()) { 223 case AppSearchResult.RESULT_INVALID_ARGUMENT -> new AppFunctionNotFoundException( 224 appSearchResult.getErrorMessage()); 225 case AppSearchResult.RESULT_IO_ERROR -> new IOException( 226 appSearchResult.getErrorMessage()); 227 case AppSearchResult.RESULT_SECURITY_ERROR -> new SecurityException( 228 appSearchResult.getErrorMessage()); 229 default -> new IllegalStateException(appSearchResult.getErrorMessage()); 230 }; 231 } 232 233 /** 234 * Throws when the app function is not found. 235 * 236 * @hide 237 */ 238 public static class AppFunctionNotFoundException extends RuntimeException { AppFunctionNotFoundException(@onNull String errorMessage)239 private AppFunctionNotFoundException(@NonNull String errorMessage) { 240 super(errorMessage); 241 } 242 } 243 } 244