1 /* 2 * Copyright (C) 2022 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 package android.adservices.topics; 17 18 import static android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_TOPICS; 19 import static android.adservices.common.AdServicesStatusUtils.SERVICE_UNAVAILABLE_ERROR_MESSAGE; 20 21 import android.adservices.common.AdServicesStatusUtils; 22 import android.adservices.common.CallerMetadata; 23 import android.adservices.common.SandboxedSdkContextUtils; 24 import android.annotation.CallbackExecutor; 25 import android.annotation.NonNull; 26 import android.annotation.RequiresPermission; 27 import android.annotation.TestApi; 28 import android.app.sdksandbox.SandboxedSdkContext; 29 import android.content.Context; 30 import android.os.Build; 31 import android.os.OutcomeReceiver; 32 import android.os.RemoteException; 33 import android.os.SystemClock; 34 import android.text.TextUtils; 35 36 import androidx.annotation.RequiresApi; 37 38 import com.android.adservices.AdServicesCommon; 39 import com.android.adservices.LoggerFactory; 40 import com.android.adservices.ServiceBinder; 41 import com.android.adservices.shared.common.exception.ServiceUnavailableException; 42 43 import java.util.ArrayList; 44 import java.util.List; 45 import java.util.Objects; 46 import java.util.concurrent.Executor; 47 48 /** 49 * TopicsManager provides APIs for App and Ad-Sdks to get the user interest topics in a privacy 50 * preserving way. 51 * 52 * <p>The instance of the {@link TopicsManager} can be obtained using {@link 53 * Context#getSystemService} and {@link TopicsManager} class. 54 */ 55 @RequiresApi(Build.VERSION_CODES.S) 56 public final class TopicsManager { 57 private static final LoggerFactory.Logger sLogger = LoggerFactory.getTopicsLogger(); 58 /** 59 * Constant that represents the service name for {@link TopicsManager} to be used in {@link 60 * android.adservices.AdServicesFrameworkInitializer#registerServiceWrappers} 61 * 62 * @hide 63 */ 64 public static final String TOPICS_SERVICE = "topics_service"; 65 66 // When an app calls the Topics API directly, it sets the SDK name to empty string. 67 static final String EMPTY_SDK = ""; 68 69 // Default value is true to record SDK's Observation when it calls Topics API. 70 static final boolean RECORD_OBSERVATION_DEFAULT = true; 71 72 private Context mContext; 73 private ServiceBinder<ITopicsService> mServiceBinder; 74 75 /** 76 * Factory method for creating an instance of TopicsManager. 77 * 78 * @param context The {@link Context} to use 79 * @return A {@link TopicsManager} instance 80 */ 81 @NonNull get(@onNull Context context)82 public static TopicsManager get(@NonNull Context context) { 83 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { 84 throw new ServiceUnavailableException(SERVICE_UNAVAILABLE_ERROR_MESSAGE); 85 } 86 // On TM+, context.getSystemService() does more than just call constructor. 87 return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) 88 ? context.getSystemService(TopicsManager.class) 89 : new TopicsManager(context); 90 } 91 92 /** 93 * Create TopicsManager 94 * 95 * @hide 96 */ TopicsManager(Context context)97 public TopicsManager(Context context) { 98 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { 99 throw new ServiceUnavailableException(SERVICE_UNAVAILABLE_ERROR_MESSAGE); 100 } 101 // In case the TopicsManager is initiated from inside a sdk_sandbox process the fields 102 // will be immediately rewritten by the initialize method below. 103 initialize(context); 104 } 105 106 /** 107 * Initializes {@link TopicsManager} with the given {@code context}. 108 * 109 * <p>This method is called by the {@link SandboxedSdkContext} to propagate the correct context. 110 * For more information check the javadoc on the {@link 111 * android.app.sdksandbox.SdkSandboxSystemServiceRegistry}. 112 * 113 * @hide 114 * @see android.app.sdksandbox.SdkSandboxSystemServiceRegistry 115 */ initialize(Context context)116 public TopicsManager initialize(Context context) { 117 mContext = context; 118 mServiceBinder = 119 ServiceBinder.getServiceBinder( 120 context, 121 AdServicesCommon.ACTION_TOPICS_SERVICE, 122 ITopicsService.Stub::asInterface); 123 return this; 124 } 125 126 @NonNull getService()127 private ITopicsService getService() { 128 ITopicsService service = mServiceBinder.getService(); 129 if (service == null) { 130 throw new ServiceUnavailableException(SERVICE_UNAVAILABLE_ERROR_MESSAGE); 131 } 132 return service; 133 } 134 135 /** 136 * Return the topics. 137 * 138 * @param getTopicsRequest The request for obtaining Topics. 139 * @param executor The executor to run callback. 140 * @param callback The callback that's called after topics are available or an error occurs. 141 * @throws IllegalStateException if this API is not available. 142 */ 143 @NonNull 144 @RequiresPermission(ACCESS_ADSERVICES_TOPICS) getTopics( @onNull GetTopicsRequest getTopicsRequest, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<GetTopicsResponse, Exception> callback)145 public void getTopics( 146 @NonNull GetTopicsRequest getTopicsRequest, 147 @NonNull @CallbackExecutor Executor executor, 148 @NonNull OutcomeReceiver<GetTopicsResponse, Exception> callback) { 149 Objects.requireNonNull(getTopicsRequest); 150 Objects.requireNonNull(executor); 151 Objects.requireNonNull(callback); 152 CallerMetadata callerMetadata = 153 new CallerMetadata.Builder() 154 .setBinderElapsedTimestamp(SystemClock.elapsedRealtime()) 155 .build(); 156 final ITopicsService service = getService(); 157 String sdkName = getTopicsRequest.getAdsSdkName(); 158 String appPackageName = ""; 159 String sdkPackageName = ""; 160 // First check if context is SandboxedSdkContext or not 161 SandboxedSdkContext sandboxedSdkContext = 162 SandboxedSdkContextUtils.getAsSandboxedSdkContext(mContext); 163 if (sandboxedSdkContext != null) { 164 // This is the case with the Sandbox. 165 sdkPackageName = sandboxedSdkContext.getSdkPackageName(); 166 appPackageName = sandboxedSdkContext.getClientPackageName(); 167 168 if (!TextUtils.isEmpty(sdkName)) { 169 throw new IllegalArgumentException( 170 "When calling Topics API from Sandbox, caller should not set Ads Sdk Name"); 171 } 172 173 String sdkNameFromSandboxedContext = sandboxedSdkContext.getSdkName(); 174 if (null == sdkNameFromSandboxedContext || sdkNameFromSandboxedContext.isEmpty()) { 175 throw new IllegalArgumentException( 176 "Sdk Name From SandboxedSdkContext should not be null or empty"); 177 } 178 179 sdkName = sdkNameFromSandboxedContext; 180 } else { 181 // This is the case without the Sandbox. 182 if (null == sdkName) { 183 // When adsSdkName is not set, we assume the App calls the Topics API directly. 184 // We set the adsSdkName to empty to mark this. 185 sdkName = EMPTY_SDK; 186 } 187 appPackageName = mContext.getPackageName(); 188 } 189 try { 190 service.getTopics( 191 new GetTopicsParam.Builder() 192 .setAppPackageName(appPackageName) 193 .setSdkName(sdkName) 194 .setSdkPackageName(sdkPackageName) 195 .setShouldRecordObservation(getTopicsRequest.shouldRecordObservation()) 196 .build(), 197 callerMetadata, 198 new IGetTopicsCallback.Stub() { 199 @Override 200 public void onResult(GetTopicsResult resultParcel) { 201 executor.execute( 202 () -> { 203 if (resultParcel.isSuccess()) { 204 callback.onResult(buildGetTopicsResponse(resultParcel)); 205 } else { 206 // TODO: Errors should be returned in onFailure method. 207 callback.onError( 208 AdServicesStatusUtils.asException( 209 resultParcel)); 210 } 211 }); 212 } 213 214 @Override 215 public void onFailure(int resultCode) { 216 executor.execute( 217 () -> 218 callback.onError( 219 AdServicesStatusUtils.asException(resultCode))); 220 } 221 }); 222 } catch (RemoteException e) { 223 sLogger.e(e, "RemoteException"); 224 callback.onError(e); 225 } 226 } 227 buildGetTopicsResponse(GetTopicsResult resultParcel)228 private GetTopicsResponse buildGetTopicsResponse(GetTopicsResult resultParcel) { 229 return new GetTopicsResponse.Builder( 230 getTopicList(resultParcel), getEncryptedTopicList(resultParcel)) 231 .build(); 232 } 233 getTopicList(GetTopicsResult resultParcel)234 private List<Topic> getTopicList(GetTopicsResult resultParcel) { 235 List<Long> taxonomyVersionsList = resultParcel.getTaxonomyVersions(); 236 List<Long> modelVersionsList = resultParcel.getModelVersions(); 237 List<Integer> topicsCodeList = resultParcel.getTopics(); 238 List<Topic> topicList = new ArrayList<>(); 239 int size = taxonomyVersionsList.size(); 240 for (int i = 0; i < size; i++) { 241 Topic topic = 242 new Topic( 243 taxonomyVersionsList.get(i), 244 modelVersionsList.get(i), 245 topicsCodeList.get(i)); 246 topicList.add(topic); 247 } 248 249 return topicList; 250 } 251 getEncryptedTopicList(GetTopicsResult resultParcel)252 private List<EncryptedTopic> getEncryptedTopicList(GetTopicsResult resultParcel) { 253 List<EncryptedTopic> encryptedTopicList = new ArrayList<>(); 254 List<byte[]> encryptedTopics = resultParcel.getEncryptedTopics(); 255 List<String> encryptionKeys = resultParcel.getEncryptionKeys(); 256 List<byte[]> encapsulatedKeys = resultParcel.getEncapsulatedKeys(); 257 int size = encryptedTopics.size(); 258 for (int i = 0; i < size; i++) { 259 EncryptedTopic encryptedTopic = 260 new EncryptedTopic( 261 encryptedTopics.get(i), encryptionKeys.get(i), encapsulatedKeys.get(i)); 262 encryptedTopicList.add(encryptedTopic); 263 } 264 265 return encryptedTopicList; 266 } 267 268 /** 269 * If the service is in an APK (as opposed to the system service), unbind it from the service to 270 * allow the APK process to die. 271 * 272 * @hide Not sure if we'll need this functionality in the final API. For now, we need it for 273 * performance testing to simulate "cold-start" situations. 274 */ 275 // TODO: change to @VisibleForTesting 276 @TestApi unbindFromService()277 public void unbindFromService() { 278 mServiceBinder.unbindFromService(); 279 } 280 } 281