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