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 com.android.adservices.service.kanon;
18 
19 import android.annotation.RequiresApi;
20 import android.content.Context;
21 import android.os.Build;
22 
23 import androidx.annotation.NonNull;
24 
25 import com.android.adservices.LoggerFactory;
26 import com.android.adservices.data.kanon.KAnonMessageConstants;
27 import com.android.adservices.service.Flags;
28 import com.android.adservices.service.stats.AdServicesLogger;
29 
30 import java.time.Clock;
31 import java.time.Instant;
32 import java.util.List;
33 import java.util.Objects;
34 import java.util.Random;
35 import java.util.stream.Collectors;
36 
37 /** KAnon sign join manager class */
38 @RequiresApi(Build.VERSION_CODES.S)
39 public class KAnonSignJoinManager {
40     private static final LoggerFactory.Logger sLogger = LoggerFactory.getKAnonLogger();
41     private final KAnonCaller mKAnonCaller;
42     private final KAnonMessageManager mKAnonMessageManager;
43     private final Flags mFlags;
44     private final Clock mClock;
45     private final AdServicesLogger mAdServicesLogger;
46     private final Context mContext;
47 
KAnonSignJoinManager( @onNull Context context, @NonNull KAnonCaller kAnonCaller, @NonNull KAnonMessageManager kAnonMessageManager, @NonNull Flags flags, @NonNull Clock clock, @NonNull AdServicesLogger adServicesLogger)48     public KAnonSignJoinManager(
49             @NonNull Context context,
50             @NonNull KAnonCaller kAnonCaller,
51             @NonNull KAnonMessageManager kAnonMessageManager,
52             @NonNull Flags flags,
53             @NonNull Clock clock,
54             @NonNull AdServicesLogger adServicesLogger) {
55         Objects.requireNonNull(kAnonCaller);
56         Objects.requireNonNull(context);
57         Objects.requireNonNull(kAnonMessageManager);
58         Objects.requireNonNull(flags);
59         Objects.requireNonNull(clock);
60         Objects.requireNonNull(adServicesLogger);
61 
62         mContext = context;
63         mKAnonCaller = kAnonCaller;
64         mKAnonMessageManager = kAnonMessageManager;
65         mFlags = flags;
66         mClock = clock;
67         mAdServicesLogger = adServicesLogger;
68     }
69 
70     /**
71      * Filters whether a message needs to be processed in the current instance. We will filter a
72      * message out for current processing if it exists in the database already and is bound to be
73      * picked up by a background process or if it has already been processed.
74      *
75      * @return {@code false} if the message is to be filtered out, and {@code true} otherwise.
76      */
filterRequest(KAnonMessageEntity kAnonMessageEntity)77     private boolean filterRequest(KAnonMessageEntity kAnonMessageEntity) {
78         sLogger.v(
79                 "Starting filter request method for message with message id: "
80                         + kAnonMessageEntity.getAdSelectionId());
81         boolean shouldProcessRightNow = false;
82         List<KAnonMessageEntity> messageEntitiesFromDB =
83                 mKAnonMessageManager.fetchKAnonMessageEntityWithMessage(
84                         kAnonMessageEntity.getHashSet());
85         // We will be making sign/join calls for this message if it doesn't exist in the database OR
86         // It exists in the database in the PROCESSED(SIGNED/JOINED) status with expired
87         // corresponding client params.
88         if (messageEntitiesFromDB.isEmpty()) {
89             sLogger.v("Message not found in the database, message should be processed");
90             shouldProcessRightNow = true;
91         } else {
92             for (KAnonMessageEntity messageInDB : messageEntitiesFromDB) {
93                 Instant clientParamsExpiryInstant =
94                         messageInDB.getCorrespondingClientParametersExpiryInstant();
95                 if (messageInDB.getStatus()
96                                 != KAnonMessageEntity.KanonMessageEntityStatus.NOT_PROCESSED
97                         && clientParamsExpiryInstant != null
98                         && clientParamsExpiryInstant.isBefore(mClock.instant())) {
99                     sLogger.v(
100                             "Message found in database but corresponding client parameters have"
101                                     + " expired, message should be processed");
102                     shouldProcessRightNow = true;
103                 }
104             }
105         }
106         if (shouldProcessRightNow) {
107             sLogger.v(
108                     "The message will be either signed/joined immediately or persisted to the "
109                             + "database for delayed processing by background job");
110 
111         } else {
112             sLogger.v(
113                     "This message has been ignored for processing because it already exists in the"
114                             + " database and the corresponding client params haven't expired");
115         }
116         return shouldProcessRightNow;
117     }
118 
119     /**
120      * This generates a boolean with probability of {@code true} equal to the given percentage X. If
121      * the randomly generated number between (0-100) is less than X, then return {@code true}
122      * otherwise return {@code false}.
123      */
shouldMakeKAnonCallsNow()124     private boolean shouldMakeKAnonCallsNow() {
125         Random random = new Random();
126         return mFlags.getFledgeKAnonPercentageImmediateSignJoinCalls() > random.nextInt(100);
127     }
128 
129     /**
130      * This method will be used to process the new {@link KAnonMessageEntity}. This will be used by
131      * {@link com.android.adservices.service.adselection.PersistAdSelectionResultRunner} to process
132      * the new ad winner/ghost ad winners.
133      */
processNewMessages(List<KAnonMessageEntity> newMessages)134     public void processNewMessages(List<KAnonMessageEntity> newMessages) {
135         try {
136             boolean forceSchedule = false;
137             KAnonSignJoinBackgroundJobService.scheduleIfNeeded(mContext, forceSchedule);
138         } catch (Throwable t) {
139             // Not throwing this error because we want this error to fail silently.
140             sLogger.e("Error while scheduling KAnon background job service:" + t.getMessage());
141         }
142         List<KAnonMessageEntity> messageAfterFiltering =
143                 newMessages.stream().filter(this::filterRequest).collect(Collectors.toList());
144         if (messageAfterFiltering.isEmpty()) {
145             return;
146         }
147         List<KAnonMessageEntity> insertedMessages =
148                 mKAnonMessageManager.persistNewAnonMessageEntities(messageAfterFiltering);
149         if (shouldMakeKAnonCallsNow()) {
150             sLogger.v("Processing message immediately from persist ad selection result API");
151             mKAnonCaller.signAndJoinMessages(
152                     insertedMessages, KAnonCaller.KAnonCallerSource.IMMEDIATE_SIGN_JOIN);
153         } else {
154             sLogger.v("Message will be picked up later by the background process");
155             // TODO(b/326903508): Remove unused loggers. Use callback instead of logger
156             // for testing.
157             mAdServicesLogger.logKAnonSignJoinStatus();
158         }
159     }
160 
161     /**
162      * This method is used by the background job. This method fetches the messages from the database
163      * and processes them by making sign join calls.
164      */
processMessagesFromDatabase(int numberOfMessages)165     public void processMessagesFromDatabase(int numberOfMessages) {
166         List<KAnonMessageEntity> messageEntities =
167                 mKAnonMessageManager.fetchNKAnonMessagesWithStatus(
168                         numberOfMessages, KAnonMessageConstants.MessageStatus.NOT_PROCESSED);
169         if (messageEntities.isEmpty()) {
170             return;
171         }
172         sLogger.v("Processing " + messageEntities.size() + " messages from database");
173         mKAnonCaller.signAndJoinMessages(
174                 messageEntities, KAnonCaller.KAnonCallerSource.BACKGROUND_JOB);
175     }
176 }
177