1 /* 2 * Copyright (C) 2023 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.cobalt; 18 19 import static com.android.adservices.AdServicesCommon.ADSERVICES_APEX_NAME_SUFFIX; 20 import static com.android.adservices.AdServicesCommon.EXTSERVICES_APEX_NAME_SUFFIX; 21 import static com.android.adservices.service.measurement.rollback.MeasurementRollbackCompatManager.APEX_VERSION_WHEN_NOT_FOUND; 22 23 import android.content.Context; 24 import android.content.pm.PackageInfo; 25 import android.content.pm.PackageManager; 26 27 import com.android.adservices.LogUtil; 28 import com.android.adservices.concurrency.AdServicesExecutors; 29 import com.android.adservices.service.Flags; 30 import com.android.cobalt.CobaltLogger; 31 import com.android.cobalt.CobaltPeriodicJob; 32 import com.android.cobalt.CobaltPipelineType; 33 import com.android.cobalt.crypto.HpkeEncrypter; 34 import com.android.cobalt.data.DataService; 35 import com.android.cobalt.domain.Project; 36 import com.android.cobalt.domain.ReportIdentifier; 37 import com.android.cobalt.impl.CobaltLoggerImpl; 38 import com.android.cobalt.impl.CobaltPeriodicJobImpl; 39 import com.android.cobalt.observations.PrivacyGenerator; 40 import com.android.cobalt.system.SystemClockImpl; 41 import com.android.cobalt.system.SystemData; 42 import com.android.internal.annotations.GuardedBy; 43 import com.android.internal.annotations.VisibleForTesting; 44 45 import com.google.common.base.Strings; 46 import com.google.common.collect.ImmutableList; 47 48 import java.security.SecureRandom; 49 import java.time.Duration; 50 import java.util.List; 51 import java.util.Objects; 52 import java.util.concurrent.ExecutorService; 53 import java.util.concurrent.ScheduledExecutorService; 54 55 /** Factory for Cobalt's logger and periodic job implementations. */ 56 public final class CobaltFactory { 57 private static final Object SINGLETON_LOCK = new Object(); 58 private static final String TAG = CobaltFactory.class.getSimpleName(); 59 60 private static final long APEX_VERSION_WHEN_NOT_FOUND = -1L; 61 62 /* 63 * Uses the prod pipeline because AdServices' reports are for either the DEBUG or GA release 64 * stage and DEBUG is sufficient for local testing. 65 */ 66 private static final CobaltPipelineType PIPELINE_TYPE = CobaltPipelineType.PROD; 67 68 // Objects which are non-trivial to construct or need to be shared between the logger and 69 // periodic job are static. 70 private static Project sSingletonCobaltRegistryProject; 71 private static DataService sSingletonDataService; 72 private static SecureRandom sSingletonSecureRandom; 73 private static SystemData sSingletonSystemData; 74 private static ImmutableList<ReportIdentifier> sSingletonReportsToIgnore; 75 76 @GuardedBy("SINGLETON_LOCK") 77 private static CobaltLogger sSingletonCobaltLogger; 78 79 @GuardedBy("SINGLETON_LOCK") 80 private static CobaltPeriodicJob sSingletonCobaltPeriodicJob; 81 82 /** 83 * Returns the static singleton CobaltLogger. 84 * 85 * @throws CobaltInitializationException if an unrecoverable errors occurs during initialization 86 */ 87 // TODO(b/311183933): Remove passed in Context from static method. 88 @SuppressWarnings("AvoidStaticContext") getCobaltLogger(Context context, Flags flags)89 public static CobaltLogger getCobaltLogger(Context context, Flags flags) 90 throws CobaltInitializationException { 91 Objects.requireNonNull(context); 92 Objects.requireNonNull(flags); 93 synchronized (SINGLETON_LOCK) { 94 if (sSingletonCobaltLogger == null) { 95 sSingletonCobaltLogger = 96 new CobaltLoggerImpl( 97 getRegistry(context, flags), 98 CobaltReleaseStages.getReleaseStage( 99 flags.getAdservicesReleaseStageForCobalt()), 100 getDataService(context, flags), 101 getSystemData(context), 102 getExecutor(), 103 new SystemClockImpl(), 104 getReportsToIgnore(flags), 105 flags.getCobaltLoggingEnabled()); 106 } 107 return sSingletonCobaltLogger; 108 } 109 } 110 111 /** 112 * Returns the static singleton CobaltPeriodicJob. 113 * 114 * <p>Note, this implementation does not result in any data being uploaded because the upload 115 * API does not exist yet and the actual uploader is blocked on it landing. 116 * 117 * @throws CobaltInitializationException if an unrecoverable errors occurs during initialization 118 */ 119 // TODO(b/311183933): Remove passed in Context from static method. 120 @SuppressWarnings("AvoidStaticContext") getCobaltPeriodicJob(Context context, Flags flags)121 public static CobaltPeriodicJob getCobaltPeriodicJob(Context context, Flags flags) 122 throws CobaltInitializationException { 123 Objects.requireNonNull(context); 124 Objects.requireNonNull(flags); 125 synchronized (SINGLETON_LOCK) { 126 if (sSingletonCobaltPeriodicJob == null) { 127 sSingletonCobaltPeriodicJob = 128 new CobaltPeriodicJobImpl( 129 getRegistry(context, flags), 130 CobaltReleaseStages.getReleaseStage( 131 flags.getAdservicesReleaseStageForCobalt()), 132 getDataService(context, flags), 133 getExecutor(), 134 getScheduledExecutor(), 135 new SystemClockImpl(), 136 getSystemData(context), 137 new PrivacyGenerator(getSecureRandom()), 138 getSecureRandom(), 139 new CobaltUploader(context, PIPELINE_TYPE), 140 HpkeEncrypter.createForEnvironment( 141 new HpkeEncryptImpl(), 142 PIPELINE_TYPE, 143 CobaltPublicKeyLoader.getInstance()), 144 CobaltApiKeys.copyFromHexApiKey( 145 flags.getCobaltAdservicesApiKeyHex()), 146 Duration.ofMillis(flags.getCobaltUploadServiceUnbindDelayMs()), 147 new CobaltOperationLoggerImpl( 148 flags.getCobaltOperationalLoggingEnabled()), 149 getReportsToIgnore(flags), 150 flags.getCobaltLoggingEnabled()); 151 } 152 return sSingletonCobaltPeriodicJob; 153 } 154 } 155 getExecutor()156 private static ExecutorService getExecutor() { 157 // Cobalt requires disk I/O and must run on the background executor. 158 return AdServicesExecutors.getBackgroundExecutor(); 159 } 160 getScheduledExecutor()161 private static ScheduledExecutorService getScheduledExecutor() { 162 // Cobalt requires a timeout to disconnect from the system server. 163 return AdServicesExecutors.getScheduler(); 164 } 165 getRegistry(Context context, Flags flags)166 private static Project getRegistry(Context context, Flags flags) 167 throws CobaltInitializationException { 168 if (sSingletonCobaltRegistryProject == null) { 169 sSingletonCobaltRegistryProject = CobaltRegistryLoader.getRegistry(context, flags); 170 } 171 return sSingletonCobaltRegistryProject; 172 } 173 getDataService(Context context, Flags flags)174 private static DataService getDataService(Context context, Flags flags) { 175 Objects.requireNonNull(context); 176 if (sSingletonDataService == null) { 177 sSingletonDataService = 178 CobaltDataServiceFactory.createDataService( 179 context, 180 getExecutor(), 181 new CobaltOperationLoggerImpl( 182 flags.getCobaltOperationalLoggingEnabled())); 183 } 184 185 return sSingletonDataService; 186 } 187 getSecureRandom()188 private static SecureRandom getSecureRandom() { 189 if (sSingletonSecureRandom == null) { 190 sSingletonSecureRandom = new SecureRandom(); 191 } 192 193 return sSingletonSecureRandom; 194 } 195 getSystemData(Context context)196 private static SystemData getSystemData(Context context) { 197 if (sSingletonSystemData == null) { 198 sSingletonSystemData = new SystemData(computeApexVersion(context)); 199 } 200 201 return sSingletonSystemData; 202 } 203 getReportsToIgnore(Flags flags)204 private static ImmutableList<ReportIdentifier> getReportsToIgnore(Flags flags) { 205 if (sSingletonReportsToIgnore == null) { 206 sSingletonReportsToIgnore = parseReportsToIgnore(flags); 207 } 208 209 return sSingletonReportsToIgnore; 210 } 211 parseReportsToIgnore(Flags flags)212 static ImmutableList<ReportIdentifier> parseReportsToIgnore(Flags flags) { 213 ImmutableList.Builder<ReportIdentifier> reportsToIgnore = ImmutableList.builder(); 214 String flag = 215 Strings.nullToEmpty(flags.getCobaltIgnoredReportIdList()).replaceAll("\\s", ""); 216 for (String reportToIgnore : flag.split(",")) { 217 String[] parts = reportToIgnore.split(":"); 218 if (parts.length != 4) { 219 LogUtil.e("Report to ignore '%s' skipped, contains too few parts", reportToIgnore); 220 continue; 221 } 222 223 try { 224 ReportIdentifier reportIdentifier = 225 ReportIdentifier.create( 226 Integer.parseInt(parts[0]), 227 Integer.parseInt(parts[1]), 228 Integer.parseInt(parts[2]), 229 Integer.parseInt(parts[3])); 230 if (reportIdentifier.customerId() >= 0 231 && reportIdentifier.projectId() >= 0 232 && reportIdentifier.metricId() >= 0 233 && reportIdentifier.reportId() >= 0) { 234 reportsToIgnore.add(reportIdentifier); 235 } else { 236 LogUtil.e( 237 "Report to ignore '%s' skipped, contains negative integer", 238 reportToIgnore); 239 } 240 } catch (NumberFormatException e) { 241 LogUtil.e(e, "Failed to parse int from report to ignore '%s'", reportToIgnore); 242 } 243 } 244 return reportsToIgnore.build(); 245 } 246 247 /** 248 * Returns the {@code Adservices} APEX version in String. If {@code Adservices} is not 249 * available, returns {@code Extservices} APEX version. Otherwise return {@code 250 * APEX_VERSION_WHEN_NOT_FOUND} if {@code Adservices} nor {@code Extservices} are not available. 251 */ 252 // TODO(b/323567786): Move this method to a common util class. 253 // TODO(b/311183933): Remove passed in Context from static method. 254 @SuppressWarnings("AvoidStaticContext") 255 @VisibleForTesting computeApexVersion(Context context)256 public static String computeApexVersion(Context context) { 257 PackageManager packageManager = context.getPackageManager(); 258 List<PackageInfo> installedPackages = 259 packageManager.getInstalledPackages(PackageManager.MATCH_APEX); 260 long adservicesVersion = APEX_VERSION_WHEN_NOT_FOUND; 261 long extservicesVersion = APEX_VERSION_WHEN_NOT_FOUND; 262 263 for (PackageInfo packageInfo : installedPackages) { 264 if (packageInfo.isApex 265 && packageInfo.packageName.endsWith(ADSERVICES_APEX_NAME_SUFFIX)) { 266 adservicesVersion = packageInfo.getLongVersionCode(); 267 return String.valueOf(adservicesVersion); 268 } else if (packageInfo.isApex 269 && packageInfo.packageName.endsWith(EXTSERVICES_APEX_NAME_SUFFIX)) { 270 extservicesVersion = packageInfo.getLongVersionCode(); 271 } 272 } 273 return String.valueOf(extservicesVersion); 274 } 275 } 276