• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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