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.shared.spe.logging; 18 19 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__FAILED_WITHOUT_RETRY; 20 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__FAILED_WITH_RETRY; 21 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__HALTED_FOR_UNKNOWN_REASON; 22 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__ONSTOP_CALLED_WITHOUT_RETRY; 23 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__ONSTOP_CALLED_WITH_RETRY; 24 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SUCCESSFUL; 25 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SPE_FAIL_TO_COMMIT_JOB_EXECUTION_START_TIME; 26 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SPE_FAIL_TO_COMMIT_JOB_EXECUTION_STOP_TIME; 27 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SPE_INVALID_EXECUTION_PERIOD; 28 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SPE_UNAVAILABLE_JOB_EXECUTION_START_TIMESTAMP; 29 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON; 30 import static com.android.adservices.shared.spe.JobServiceConstants.EXECUTION_LOGGING_UNKNOWN_MODULE_NAME; 31 import static com.android.adservices.shared.spe.JobServiceConstants.MAX_PERCENTAGE; 32 import static com.android.adservices.shared.spe.JobServiceConstants.MILLISECONDS_PER_MINUTE; 33 import static com.android.adservices.shared.spe.JobServiceConstants.SHARED_PREFS_BACKGROUND_JOBS; 34 import static com.android.adservices.shared.spe.JobServiceConstants.UNAVAILABLE_JOB_EXECUTION_PERIOD; 35 import static com.android.adservices.shared.spe.JobServiceConstants.UNAVAILABLE_JOB_EXECUTION_START_TIMESTAMP; 36 import static com.android.adservices.shared.spe.JobServiceConstants.UNAVAILABLE_JOB_EXECUTION_STOP_TIMESTAMP; 37 import static com.android.adservices.shared.spe.JobServiceConstants.UNAVAILABLE_JOB_LATENCY; 38 import static com.android.adservices.shared.spe.JobServiceConstants.UNAVAILABLE_STOP_REASON; 39 import static com.android.adservices.shared.spe.framework.ExecutionResult.FAILURE_WITHOUT_RETRY; 40 import static com.android.adservices.shared.spe.framework.ExecutionResult.FAILURE_WITH_RETRY; 41 import static com.android.adservices.shared.spe.framework.ExecutionResult.SUCCESS; 42 import static com.android.adservices.shared.util.LogUtil.VERBOSE; 43 44 import android.annotation.NonNull; 45 import android.annotation.TargetApi; 46 import android.app.job.JobParameters; 47 import android.app.job.JobService; 48 import android.content.Context; 49 import android.content.SharedPreferences; 50 import android.os.Build; 51 52 import com.android.adservices.shared.common.flags.ModuleSharedFlags; 53 import com.android.adservices.shared.errorlogging.AdServicesErrorLogger; 54 import com.android.adservices.shared.spe.JobServiceConstants; 55 import com.android.adservices.shared.spe.framework.AbstractJobService; 56 import com.android.adservices.shared.spe.framework.ExecutionResult; 57 import com.android.adservices.shared.util.Clock; 58 import com.android.adservices.shared.util.LogUtil; 59 import com.android.internal.annotations.VisibleForTesting; 60 import com.android.modules.utils.build.SdkLevel; 61 62 import com.google.common.util.concurrent.MoreExecutors; 63 64 import java.util.Map; 65 import java.util.Random; 66 import java.util.concurrent.Executor; 67 import java.util.concurrent.locks.ReadWriteLock; 68 import java.util.concurrent.locks.ReentrantReadWriteLock; 69 70 /** Class for logging methods used by background jobs. */ 71 // TODO(b/325292968): make this class final after all Jobs migrated to using SPE. 72 public class JobServiceLogger { 73 private static final ReadWriteLock sReadWriteLock = new ReentrantReadWriteLock(); 74 private static final Random sRandom = new Random(); 75 76 private final Context mContext; 77 private final Clock mClock; 78 private final StatsdJobServiceLogger mStatsdLogger; 79 private final AdServicesErrorLogger mErrorLogger; 80 // JobService runs the execution on the main thread, so the logging part should be offloaded to 81 // a separated thread. However, these logging events should be in sequence, respecting to the 82 // start and the end of an execution. 83 private final Executor mLoggingExecutor; 84 private final Map<Integer, String> mJobInfoMap; 85 private final ModuleSharedFlags mFlags; 86 87 /** Create an instance of {@link JobServiceLogger}. */ JobServiceLogger( Context context, Clock clock, StatsdJobServiceLogger statsdLogger, AdServicesErrorLogger errorLogger, Executor executor, Map<Integer, String> jobIdToNameMap, ModuleSharedFlags flags)88 public JobServiceLogger( 89 Context context, 90 Clock clock, 91 StatsdJobServiceLogger statsdLogger, 92 AdServicesErrorLogger errorLogger, 93 Executor executor, 94 Map<Integer, String> jobIdToNameMap, 95 ModuleSharedFlags flags) { 96 mContext = context; 97 mClock = clock; 98 mStatsdLogger = statsdLogger; 99 mErrorLogger = errorLogger; 100 mLoggingExecutor = MoreExecutors.newSequentialExecutor(executor); 101 mJobInfoMap = jobIdToNameMap; 102 mFlags = flags; 103 } 104 105 /** 106 * {@link JobService} calls this method in {@link JobService#onStartJob(JobParameters)} to 107 * record that onStartJob was called. 108 * 109 * @param jobId the unique id of the job to log for. 110 */ recordOnStartJob(int jobId)111 public void recordOnStartJob(int jobId) { 112 if (!mFlags.getBackgroundJobsLoggingEnabled()) { 113 return; 114 } 115 116 long startJobTimestamp = mClock.currentTimeMillis(); 117 118 mLoggingExecutor.execute(() -> persistJobExecutionData(jobId, startJobTimestamp)); 119 } 120 121 /** 122 * Records that the {@link JobService#jobFinished(JobParameters, boolean)} is called or is about 123 * to be called. 124 * 125 * @param jobId the unique id of the job to log for. 126 * @param isSuccessful indicates if the execution is successful. 127 * @param shouldRetry indicates whether to retry the execution. 128 */ 129 // TODO(b/325292968): make this method private once all jobs migrated to using SPE. recordJobFinished(int jobId, boolean isSuccessful, boolean shouldRetry)130 public void recordJobFinished(int jobId, boolean isSuccessful, boolean shouldRetry) { 131 if (!mFlags.getBackgroundJobsLoggingEnabled()) { 132 return; 133 } 134 135 int resultCode = 136 isSuccessful 137 ? AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SUCCESSFUL 138 : (shouldRetry 139 ? AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__FAILED_WITH_RETRY 140 : AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__FAILED_WITHOUT_RETRY); 141 142 mLoggingExecutor.execute( 143 () -> 144 logExecutionStats( 145 jobId, 146 mClock.currentTimeMillis(), 147 resultCode, 148 UNAVAILABLE_STOP_REASON)); 149 } 150 151 /** 152 * Records that the {@link JobService#jobFinished(JobParameters, boolean)} is called or is about 153 * to be called. 154 * 155 * <p>This is used by {@link AbstractJobService}, a part of SPE (Scheduling Policy Engine) 156 * framework. 157 * 158 * @param jobId the unique id of the job to log for. 159 * @param executionResult the {@link ExecutionResult} for current execution. 160 */ recordJobFinished(int jobId, ExecutionResult executionResult)161 public void recordJobFinished(int jobId, ExecutionResult executionResult) { 162 if (!mFlags.getBackgroundJobsLoggingEnabled()) { 163 return; 164 } 165 166 boolean isSuccessful = false; 167 boolean shouldRetry = false; 168 169 if (executionResult.equals(SUCCESS)) { 170 isSuccessful = true; 171 } else if (executionResult.equals(FAILURE_WITH_RETRY)) { 172 shouldRetry = true; 173 } else if (!executionResult.equals(FAILURE_WITHOUT_RETRY)) { 174 // Throws if the execution result to log is not one of SUCCESS, FAILURE_WITH_RETRY, or 175 // FAILURE_WITHOUT_RETRY. 176 throw new IllegalStateException( 177 "Invalid ExecutionResult: " + executionResult + ", jobId: " + jobId); 178 } 179 180 recordJobFinished(jobId, isSuccessful, shouldRetry); 181 } 182 183 /** 184 * {@link JobService} calls this method in {@link JobService#onStopJob(JobParameters)}} to 185 * enable logging. 186 * 187 * @param params configured {@link JobParameters} 188 * @param jobId the unique id of the job to log for. 189 * @param shouldRetry whether to reschedule the job. 190 */ 191 @TargetApi(Build.VERSION_CODES.S) recordOnStopJob(@onNull JobParameters params, int jobId, boolean shouldRetry)192 public void recordOnStopJob(@NonNull JobParameters params, int jobId, boolean shouldRetry) { 193 if (!mFlags.getBackgroundJobsLoggingEnabled()) { 194 return; 195 } 196 197 long endJobTimestamp = mClock.currentTimeMillis(); 198 199 // StopReason is only supported for Android Version S+. 200 int stopReason = SdkLevel.isAtLeastS() ? params.getStopReason() : UNAVAILABLE_STOP_REASON; 201 202 int resultCode = 203 shouldRetry 204 ? AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__ONSTOP_CALLED_WITH_RETRY 205 : AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__ONSTOP_CALLED_WITHOUT_RETRY; 206 207 mLoggingExecutor.execute( 208 () -> logExecutionStats(jobId, endJobTimestamp, resultCode, stopReason)); 209 } 210 211 /** 212 * Log when the execution is skipped due to customized reasons. 213 * 214 * @param jobId the unique id of the job to log for 215 * @param skipReason the result to skip the execution 216 */ recordJobSkipped(int jobId, int skipReason)217 public void recordJobSkipped(int jobId, int skipReason) { 218 if (!mFlags.getBackgroundJobsLoggingEnabled()) { 219 return; 220 } 221 222 mLoggingExecutor.execute( 223 () -> 224 logExecutionStats( 225 jobId, 226 mClock.currentTimeMillis(), 227 skipReason, 228 UNAVAILABLE_STOP_REASON)); 229 } 230 231 /** 232 * Log for various lifecycles of an execution. 233 * 234 * <p>a completed lifecycle includes job finished in {@link 235 * JobService#jobFinished(JobParameters, boolean)} or {@link 236 * JobService#onStopJob(JobParameters)}. 237 * 238 * @param jobId the job id 239 * @param jobStopExecutionTimestamp the timestamp of the end of an execution. Note it can happen 240 * in either {@link JobService#jobFinished(JobParameters, boolean)} or {@link 241 * JobService#onStopJob(JobParameters)}. 242 * @param executionResultCode the result code for current execution 243 * @param possibleStopReason if {@link JobService#onStopJob(JobParameters)} is invoked. Set 244 * {@link JobServiceConstants#UNAVAILABLE_STOP_REASON} if {@link 245 * JobService#onStopJob(JobParameters)} is not invoked. 246 */ 247 @VisibleForTesting logExecutionStats( int jobId, long jobStopExecutionTimestamp, int executionResultCode, int possibleStopReason)248 public void logExecutionStats( 249 int jobId, 250 long jobStopExecutionTimestamp, 251 int executionResultCode, 252 int possibleStopReason) { 253 String jobStartTimestampKey = getJobStartTimestampKey(jobId); 254 String executionPeriodKey = getExecutionPeriodKey(jobId); 255 String jobStopTimestampKey = getJobStopTimestampKey(jobId); 256 257 SharedPreferences sharedPreferences = getPrefs(); 258 SharedPreferences.Editor editor = sharedPreferences.edit(); 259 260 long jobStartExecutionTimestamp; 261 long jobExecutionPeriodMs; 262 263 sReadWriteLock.readLock().lock(); 264 try { 265 266 jobStartExecutionTimestamp = 267 sharedPreferences.getLong( 268 jobStartTimestampKey, UNAVAILABLE_JOB_EXECUTION_START_TIMESTAMP); 269 270 jobExecutionPeriodMs = 271 sharedPreferences.getLong(executionPeriodKey, UNAVAILABLE_JOB_EXECUTION_PERIOD); 272 } finally { 273 sReadWriteLock.readLock().unlock(); 274 } 275 276 // Stop telemetry the metrics and log error in logcat if the stat is not valid. 277 if (jobStartExecutionTimestamp == UNAVAILABLE_JOB_EXECUTION_START_TIMESTAMP 278 || jobStartExecutionTimestamp > jobStopExecutionTimestamp) { 279 LogUtil.e( 280 "Execution Stat is INVALID for job %s, jobStartTimestamp: %d, jobStopTimestamp:" 281 + " %d.", 282 mJobInfoMap.get(jobId), jobStartExecutionTimestamp, jobStopExecutionTimestamp); 283 mErrorLogger.logError( 284 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SPE_UNAVAILABLE_JOB_EXECUTION_START_TIMESTAMP, 285 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON); 286 return; 287 } 288 289 // Compute the execution latency. 290 long executionLatencyMs = jobStopExecutionTimestamp - jobStartExecutionTimestamp; 291 292 // Update jobStopExecutionTimestamp in storage. 293 editor.putLong(jobStopTimestampKey, jobStopExecutionTimestamp); 294 295 sReadWriteLock.writeLock().lock(); 296 try { 297 if (!editor.commit()) { 298 // The commitment failure should be rare. It may result in 1 problematic data but 299 // the impact could be ignored compared to a job's lifecycle. 300 LogUtil.e( 301 "Failed to update job Ending Execution Logging Data for Job %s, Job ID =" 302 + " %d.", 303 mJobInfoMap.get(jobId), jobId); 304 mErrorLogger.logError( 305 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SPE_FAIL_TO_COMMIT_JOB_EXECUTION_STOP_TIME, 306 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON); 307 } 308 } finally { 309 sReadWriteLock.writeLock().unlock(); 310 } 311 312 // Actually upload the metrics to statsD. 313 logJobStatsHelper( 314 jobId, 315 executionLatencyMs, 316 jobExecutionPeriodMs, 317 executionResultCode, 318 possibleStopReason); 319 } 320 321 /** 322 * Do background job telemetry. 323 * 324 * @param jobId the job ID 325 * @param executionLatencyMs the latency of an execution. Defined as the difference of timestamp 326 * between end and start of an execution. 327 * @param executionPeriodMs the execution period. Defined as the difference of timestamp between 328 * current and previous start of an execution. This is only valid for periodical jobs to 329 * monitor the difference between actual and configured execution period. 330 * @param resultCode the result code of an execution 331 * @param stopReason {@link JobParameters#getStopReason()} if {@link 332 * JobService#onStopJob(JobParameters)} is invoked. Otherwise, set it to {@link 333 * JobServiceConstants#UNAVAILABLE_STOP_REASON}. 334 */ 335 @VisibleForTesting logJobStatsHelper( int jobId, long executionLatencyMs, long executionPeriodMs, int resultCode, int stopReason)336 public void logJobStatsHelper( 337 int jobId, 338 long executionLatencyMs, 339 long executionPeriodMs, 340 int resultCode, 341 int stopReason) { 342 if (!shouldLog()) { 343 if (VERBOSE) { 344 LogUtil.v( 345 "This background job logging isn't selected for sampling logging, skip..."); 346 } 347 return; 348 } 349 350 // Since the execution period will be logged with unit of minute, it will be converted to 0 351 // if less than MILLISECONDS_PER_MINUTE. The negative period has two scenarios: 1) the first 352 // execution (as -1) or 2) invalid, which will be logged by CEL. As all negative values will 353 // be filtered out in the server's metric, keep the original value of them, to avoid a 0 354 // value due to small negative values. 355 long executionPeriodMinute = 356 executionPeriodMs >= 0 357 ? executionPeriodMs / MILLISECONDS_PER_MINUTE 358 : executionPeriodMs; 359 360 ExecutionReportedStats stats = 361 ExecutionReportedStats.builder() 362 .setJobId(jobId) 363 .setExecutionLatencyMs(convertLongToInteger(executionLatencyMs)) 364 .setExecutionPeriodMinute(convertLongToInteger(executionPeriodMinute)) 365 .setExecutionResultCode(resultCode) 366 .setStopReason(stopReason) 367 // TODO(b/324323522): Populate correct module name. 368 .setModuleName(EXECUTION_LOGGING_UNKNOWN_MODULE_NAME) 369 .build(); 370 mStatsdLogger.logExecutionReportedStats(stats); 371 372 if (VERBOSE) { 373 LogUtil.v( 374 "[Background job execution logging] jobId: %d, executionLatencyInMs: %d," 375 + " executionPeriodInMs: %d, resultCode: %d, stopReason: %d, moduleName:" 376 + " %d", 377 jobId, 378 executionLatencyMs, 379 executionPeriodMs, 380 resultCode, 381 stopReason, 382 EXECUTION_LOGGING_UNKNOWN_MODULE_NAME); 383 } 384 } 385 386 /** 387 * Compute execution data such as latency and period then store the data in persistent so that 388 * we can compute the job stats later. Store start job timestamp and execution period into the 389 * storage. 390 * 391 * @param jobId the job id 392 * @param startJobTimestamp the timestamp when {@link JobService#onStartJob(JobParameters)} is 393 * invoked. 394 */ 395 @VisibleForTesting persistJobExecutionData(int jobId, long startJobTimestamp)396 public void persistJobExecutionData(int jobId, long startJobTimestamp) { 397 SharedPreferences sharedPreferences = getPrefs(); 398 399 String jobStartTimestampKey = getJobStartTimestampKey(jobId); 400 String executionPeriodKey = getExecutionPeriodKey(jobId); 401 String jobStopTimestampKey = getJobStopTimestampKey(jobId); 402 403 // When onStartJob() is invoked, the data stored in the shared preference is for previous 404 // execution. 405 // 406 // JobService is scheduled as JobStatus in JobScheduler infra. Before a JobStatus instance 407 // is pushed to pendingJobQueue, it checks a few criteria like whether a same JobStatus is 408 // ready to execute, not pending, not running, etc. To determine if two JobStatus instances 409 // are same, it checks jobId, callingUid (the package that schedules the job). Therefore, 410 // there won't have two pending/running job instances with a same jobId. For more details, 411 // please check source code of JobScheduler. 412 long previousJobStartTimestamp; 413 long previousJobStopTimestamp; 414 long previousExecutionPeriod; 415 416 sReadWriteLock.readLock().lock(); 417 try { 418 previousJobStartTimestamp = 419 sharedPreferences.getLong( 420 jobStartTimestampKey, UNAVAILABLE_JOB_EXECUTION_START_TIMESTAMP); 421 previousJobStopTimestamp = 422 sharedPreferences.getLong( 423 jobStopTimestampKey, UNAVAILABLE_JOB_EXECUTION_STOP_TIMESTAMP); 424 previousExecutionPeriod = 425 sharedPreferences.getLong(executionPeriodKey, UNAVAILABLE_JOB_EXECUTION_PERIOD); 426 } finally { 427 sReadWriteLock.readLock().unlock(); 428 } 429 430 SharedPreferences.Editor editor = sharedPreferences.edit(); 431 432 // The first execution, pass execution period with UNAVAILABLE_JOB_EXECUTION_PERIOD. 433 if (previousJobStartTimestamp == UNAVAILABLE_JOB_EXECUTION_START_TIMESTAMP) { 434 editor.putLong(executionPeriodKey, UNAVAILABLE_JOB_EXECUTION_PERIOD); 435 } else { 436 // If previousJobStartTimestamp is later than previousJobStopTimestamp, it indicates the 437 // last execution didn't finish with calling jobFinished() or onStopJob(). In this case, 438 // we log as an unknown issue, which may come from system/device. 439 if (previousJobStartTimestamp > previousJobStopTimestamp) { 440 logJobStatsHelper( 441 jobId, 442 UNAVAILABLE_JOB_LATENCY, 443 previousExecutionPeriod, 444 AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__HALTED_FOR_UNKNOWN_REASON, 445 UNAVAILABLE_STOP_REASON); 446 } 447 448 // Compute execution period if there has been multiple executions. 449 // Define the execution period = difference of the timestamp of two consecutive 450 // invocations of onStartJob(). 451 long executionPeriodInMs = startJobTimestamp - previousJobStartTimestamp; 452 if (executionPeriodInMs < 0) { 453 LogUtil.e( 454 "Invalid execution period = %d! Start time for current execution should be" 455 + " later than previous execution!", 456 executionPeriodInMs); 457 mErrorLogger.logError( 458 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SPE_INVALID_EXECUTION_PERIOD, 459 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON); 460 } 461 462 // Store the execution period into shared preference. 463 editor.putLong(executionPeriodKey, executionPeriodInMs); 464 } 465 // Store current JobStartTimestamp into shared preference. 466 editor.putLong(jobStartTimestampKey, startJobTimestamp); 467 468 sReadWriteLock.writeLock().lock(); 469 try { 470 if (!editor.commit()) { 471 // The commitment failure should be rare. It may result in 1 problematic data but 472 // the impact could be ignored compared to a job's lifecycle. 473 LogUtil.e( 474 "Failed to update onStartJob() Logging Data for Job %s, Job ID = %d", 475 mJobInfoMap.get(jobId), jobId); 476 mErrorLogger.logError( 477 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SPE_FAIL_TO_COMMIT_JOB_EXECUTION_START_TIME, 478 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON); 479 } 480 } finally { 481 sReadWriteLock.writeLock().unlock(); 482 } 483 } 484 485 @VisibleForTesting getJobStartTimestampKey(int jobId)486 static String getJobStartTimestampKey(int jobId) { 487 return jobId + JobServiceConstants.SHARED_PREFS_START_TIMESTAMP_SUFFIX; 488 } 489 490 @VisibleForTesting getJobStopTimestampKey(int jobId)491 static String getJobStopTimestampKey(int jobId) { 492 return jobId + JobServiceConstants.SHARED_PREFS_STOP_TIMESTAMP_SUFFIX; 493 } 494 495 @VisibleForTesting getExecutionPeriodKey(int jobId)496 static String getExecutionPeriodKey(int jobId) { 497 return jobId + JobServiceConstants.SHARED_PREFS_EXEC_PERIOD_SUFFIX; 498 } 499 500 // Convert a long value to an integer. 501 // 502 // Used to convert a time period in long-format but needs to be logged with integer-format. 503 // Generally, a time period should always be a positive integer with a proper design of its 504 // unit. 505 // 506 // Defensively use this method to avoid any Exception. 507 @VisibleForTesting convertLongToInteger(long longVal)508 static int convertLongToInteger(long longVal) { 509 int intValue; 510 511 // The given time period should always be in the range of positive integer. Defensively 512 // handle overflow values to avoid potential Exceptions. 513 if (longVal <= Integer.MIN_VALUE) { 514 intValue = Integer.MIN_VALUE; 515 } else if (longVal >= Integer.MAX_VALUE) { 516 intValue = Integer.MAX_VALUE; 517 } else { 518 intValue = (int) longVal; 519 } 520 521 return intValue; 522 } 523 524 // Make a random draw to determine if a logging event should be uploaded t0 the logging server. 525 @VisibleForTesting shouldLog()526 boolean shouldLog() { 527 int loggingRatio = mFlags.getBackgroundJobSamplingLoggingRate(); 528 529 return sRandom.nextInt(MAX_PERCENTAGE) < loggingRatio; 530 } 531 532 @SuppressWarnings("AvoidSharedPreferences") // Legacy usage getPrefs()533 private SharedPreferences getPrefs() { 534 return mContext.getSharedPreferences(SHARED_PREFS_BACKGROUND_JOBS, Context.MODE_PRIVATE); 535 } 536 } 537