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 
17 package com.android.adservices.data.customaudience;
18 
19 import android.adservices.common.AdTechIdentifier;
20 import android.net.Uri;
21 
22 import androidx.annotation.NonNull;
23 import androidx.room.ColumnInfo;
24 import androidx.room.Entity;
25 
26 import com.android.adservices.service.Flags;
27 import com.android.adservices.service.FlagsFactory;
28 import com.android.adservices.service.customaudience.CustomAudienceUpdatableData;
29 import com.android.internal.annotations.VisibleForTesting;
30 import com.android.internal.util.Preconditions;
31 
32 import com.google.auto.value.AutoValue;
33 
34 import java.time.Instant;
35 import java.util.Objects;
36 
37 /** This POJO represents the schema for the background fetch data for custom audiences. */
38 @AutoValue
39 @AutoValue.CopyAnnotations
40 @Entity(
41         tableName = DBCustomAudienceBackgroundFetchData.TABLE_NAME,
42         primaryKeys = {"owner", "buyer", "name"},
43         inheritSuperIndices = true)
44 public abstract class DBCustomAudienceBackgroundFetchData {
45     public static final String TABLE_NAME = "custom_audience_background_fetch_data";
46 
47     /** @return the owner package name of the custom audience */
48     @AutoValue.CopyAnnotations
49     @ColumnInfo(name = "owner", index = true)
50     @NonNull
getOwner()51     public abstract String getOwner();
52 
53     /** @return the buyer for the custom audience */
54     @AutoValue.CopyAnnotations
55     @ColumnInfo(name = "buyer", index = true)
56     @NonNull
getBuyer()57     public abstract AdTechIdentifier getBuyer();
58 
59     /** @return the name of the custom audience */
60     @AutoValue.CopyAnnotations
61     @ColumnInfo(name = "name", index = true)
62     @NonNull
getName()63     public abstract String getName();
64 
65     /** @return the daily update URI for the custom audience */
66     @AutoValue.CopyAnnotations
67     @ColumnInfo(name = "daily_update_uri")
68     @NonNull
getDailyUpdateUri()69     public abstract Uri getDailyUpdateUri();
70 
71     /** @return the time after which the specified custom audience is eligible to be updated */
72     @AutoValue.CopyAnnotations
73     @ColumnInfo(name = "eligible_update_time", index = true)
74     @NonNull
getEligibleUpdateTime()75     public abstract Instant getEligibleUpdateTime();
76 
77     /**
78      * @return the number of failures since the last successful update caused by response validation
79      */
80     @AutoValue.CopyAnnotations
81     @ColumnInfo(name = "num_validation_failures")
getNumValidationFailures()82     public abstract long getNumValidationFailures();
83 
84     /** @return the number of failures since the last successful update caused by fetch timeouts */
85     @AutoValue.CopyAnnotations
86     @ColumnInfo(name = "num_timeout_failures")
getNumTimeoutFailures()87     public abstract long getNumTimeoutFailures();
88 
89     /**
90      * @return if the custom audience is considered debuggable (i.e. was created by an app where
91      *     android:debuggable="true" while developer options are enabled.
92      */
93     @AutoValue.CopyAnnotations
94     @ColumnInfo(name = "is_debuggable", defaultValue = "false")
getIsDebuggable()95     public abstract boolean getIsDebuggable();
96 
97     /** @return an AutoValue builder for a {@link DBCustomAudienceBackgroundFetchData} object */
98     @NonNull
builder()99     public static DBCustomAudienceBackgroundFetchData.Builder builder() {
100         return new AutoValue_DBCustomAudienceBackgroundFetchData.Builder()
101                 .setNumValidationFailures(0)
102                 .setNumTimeoutFailures(0)
103                 .setIsDebuggable(false);
104     }
105 
106     /**
107      * Creates a {@link DBCustomAudienceBackgroundFetchData} object using the builder.
108      *
109      * <p>Required for Room SQLite integration.
110      */
111     @NonNull
create( @onNull String owner, @NonNull AdTechIdentifier buyer, @NonNull String name, @NonNull Uri dailyUpdateUri, @NonNull Instant eligibleUpdateTime, long numValidationFailures, long numTimeoutFailures, boolean debuggable)112     public static DBCustomAudienceBackgroundFetchData create(
113             @NonNull String owner,
114             @NonNull AdTechIdentifier buyer,
115             @NonNull String name,
116             @NonNull Uri dailyUpdateUri,
117             @NonNull Instant eligibleUpdateTime,
118             long numValidationFailures,
119             long numTimeoutFailures,
120             boolean debuggable) {
121         return builder()
122                 .setOwner(owner)
123                 .setBuyer(buyer)
124                 .setName(name)
125                 .setDailyUpdateUri(dailyUpdateUri)
126                 .setEligibleUpdateTime(eligibleUpdateTime)
127                 .setNumValidationFailures(numValidationFailures)
128                 .setNumTimeoutFailures(numTimeoutFailures)
129                 .setIsDebuggable(debuggable)
130                 .build();
131     }
132 
133     /**
134      * Computes the next eligible update time, given the most recent successful update time and
135      * flags.
136      *
137      * <p>This method is split out for testing because testing with P/H flags in a static method
138      * requires non-trivial permissions in test applications.
139      */
140     @VisibleForTesting
141     @NonNull
computeNextEligibleUpdateTimeAfterSuccessfulUpdate( @onNull Instant successfulUpdateTime, @NonNull Flags flags)142     public static Instant computeNextEligibleUpdateTimeAfterSuccessfulUpdate(
143             @NonNull Instant successfulUpdateTime, @NonNull Flags flags) {
144         Objects.requireNonNull(successfulUpdateTime);
145         Objects.requireNonNull(flags);
146 
147         // Successful updates are next eligible in base interval (one day) + jitter
148         // TODO(b/221861706): Implement jitter
149         return successfulUpdateTime.plusSeconds(
150                 flags.getFledgeBackgroundFetchEligibleUpdateBaseIntervalS());
151     }
152 
153     /** Computes the next eligible update time, given the most recent successful update time. */
154     @NonNull
computeNextEligibleUpdateTimeAfterSuccessfulUpdate( @onNull Instant successfulUpdateTime)155     public static Instant computeNextEligibleUpdateTimeAfterSuccessfulUpdate(
156             @NonNull Instant successfulUpdateTime) {
157         return computeNextEligibleUpdateTimeAfterSuccessfulUpdate(
158                 successfulUpdateTime, FlagsFactory.getFlags());
159     }
160 
161     /**
162      * Creates a copy of the current object with an updated failure count based on the input failure
163      * type.
164      */
165     @NonNull
copyWithUpdatableData( @onNull CustomAudienceUpdatableData updatableData)166     public final DBCustomAudienceBackgroundFetchData copyWithUpdatableData(
167             @NonNull CustomAudienceUpdatableData updatableData) {
168         // Create a builder with a full copy of the current object
169         DBCustomAudienceBackgroundFetchData.Builder fetchDataBuilder =
170                 builder()
171                         .setOwner(getOwner())
172                         .setBuyer(getBuyer())
173                         .setName(getName())
174                         .setDailyUpdateUri(getDailyUpdateUri())
175                         .setEligibleUpdateTime(getEligibleUpdateTime())
176                         .setNumValidationFailures(getNumValidationFailures())
177                         .setNumTimeoutFailures(getNumTimeoutFailures())
178                         .setIsDebuggable(getIsDebuggable());
179 
180         if (updatableData.getContainsSuccessfulUpdate()) {
181             fetchDataBuilder.setEligibleUpdateTime(
182                     computeNextEligibleUpdateTimeAfterSuccessfulUpdate(
183                             updatableData.getAttemptedUpdateTime()));
184 
185             // Reset all failure counts on any successful update
186             fetchDataBuilder.setNumValidationFailures(0).setNumTimeoutFailures(0);
187         } else {
188             switch (updatableData.getInitialUpdateResult()) {
189                 case SUCCESS:
190                     // This success result is only the result set prior to syntax validation of the
191                     // response, so an error must have occurred during data validation
192                     // INTENTIONAL FALLTHROUGH
193                 case RESPONSE_VALIDATION_FAILURE:
194                     fetchDataBuilder.setNumValidationFailures(getNumValidationFailures() + 1);
195                     break;
196                 case NETWORK_FAILURE:
197                     // TODO(b/221861706): Consider differentiating timeout failures for fairness
198                     // TODO(b/237342352): Consolidate timeout failures if they don't need to be
199                     //  distinguished
200                     // INTENTIONAL FALLTHROUGH
201                 case NETWORK_READ_TIMEOUT_FAILURE:
202                     fetchDataBuilder.setNumTimeoutFailures(getNumTimeoutFailures() + 1);
203                     break;
204                 case K_ANON_FAILURE:
205                     // TODO(b/234884352): Implement k-anon check
206                     // INTENTIONAL FALLTHROUGH
207                 case UNKNOWN:
208                     // Treat this as a benign failure, so we can just try again
209                     break;
210             }
211 
212             // TODO(b/221861706): Decide whether this custom audience is delinquent and set its next
213             //  eligible update time
214             // TODO(b/221861706): Implement jitter for delinquent updates
215 
216             // Non-delinquent failed updates are immediately eligible to be updated in the next job
217             fetchDataBuilder.setEligibleUpdateTime(getEligibleUpdateTime());
218         }
219 
220         return fetchDataBuilder.build();
221     }
222 
223     /** Builder class for a {@link DBCustomAudienceBackgroundFetchData} object. */
224     @AutoValue.Builder
225     public abstract static class Builder {
226         /** Sets the owner package name for the custom audience. */
227         @NonNull
setOwner(@onNull String value)228         public abstract Builder setOwner(@NonNull String value);
229 
230         /** Sets the buyer for the custom audience. */
231         @NonNull
setBuyer(@onNull AdTechIdentifier value)232         public abstract Builder setBuyer(@NonNull AdTechIdentifier value);
233 
234         /** Sets the name for the custom audience. */
235         @NonNull
setName(@onNull String value)236         public abstract Builder setName(@NonNull String value);
237 
238         /** Sets the daily update URI for the custom audience. */
239         @NonNull
setDailyUpdateUri(@onNull Uri value)240         public abstract Builder setDailyUpdateUri(@NonNull Uri value);
241 
242         /** Sets the time after which the custom audience will be eligible for update. */
243         @NonNull
setEligibleUpdateTime(@onNull Instant value)244         public abstract Builder setEligibleUpdateTime(@NonNull Instant value);
245 
246         /**
247          * Sets the number of failures due to response validation for the custom audience since the
248          * last successful update.
249          */
250         @NonNull
setNumValidationFailures(long value)251         public abstract Builder setNumValidationFailures(long value);
252 
253         /**
254          * Sets the number of failures due to fetch timeout for the custom audience since the last
255          * successful update.
256          */
257         @NonNull
setNumTimeoutFailures(long value)258         public abstract Builder setNumTimeoutFailures(long value);
259 
260         /** Sets if the custom audience was created in a debuggable context. */
261         @NonNull
setIsDebuggable(boolean value)262         public abstract Builder setIsDebuggable(boolean value);
263 
264         /**
265          * Builds the {@link DBCustomAudienceBackgroundFetchData} object and returns it.
266          *
267          * <p>Note that AutoValue doesn't by itself do any validation, so splitting the builder with
268          * a manual verification is recommended. See go/autovalue/builders-howto#validate for more
269          * information.
270          */
271         @NonNull
autoValueBuild()272         protected abstract DBCustomAudienceBackgroundFetchData autoValueBuild();
273 
274         /**
275          * Builds, validates, and returns the {@link DBCustomAudienceBackgroundFetchData} object.
276          */
277         @NonNull
build()278         public final DBCustomAudienceBackgroundFetchData build() {
279             DBCustomAudienceBackgroundFetchData fetchData = autoValueBuild();
280 
281             // Fields marked @NonNull are already validated by AutoValue
282             Preconditions.checkArgument(
283                     fetchData.getNumValidationFailures() >= 0
284                             && fetchData.getNumTimeoutFailures() >= 0,
285                     "Update failure count must be non-negative");
286 
287             return fetchData;
288         }
289     }
290 }
291