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 =
258                 mContext.getSharedPreferences(SHARED_PREFS_BACKGROUND_JOBS, Context.MODE_PRIVATE);
259         SharedPreferences.Editor editor = sharedPreferences.edit();
260 
261         long jobStartExecutionTimestamp;
262         long jobExecutionPeriodMs;
263 
264         sReadWriteLock.readLock().lock();
265         try {
266 
267             jobStartExecutionTimestamp =
268                     sharedPreferences.getLong(
269                             jobStartTimestampKey, UNAVAILABLE_JOB_EXECUTION_START_TIMESTAMP);
270 
271             jobExecutionPeriodMs =
272                     sharedPreferences.getLong(executionPeriodKey, UNAVAILABLE_JOB_EXECUTION_PERIOD);
273         } finally {
274             sReadWriteLock.readLock().unlock();
275         }
276 
277         // Stop telemetry the metrics and log error in logcat if the stat is not valid.
278         if (jobStartExecutionTimestamp == UNAVAILABLE_JOB_EXECUTION_START_TIMESTAMP
279                 || jobStartExecutionTimestamp > jobStopExecutionTimestamp) {
280             LogUtil.e(
281                     "Execution Stat is INVALID for job %s, jobStartTimestamp: %d, jobStopTimestamp:"
282                             + " %d.",
283                     mJobInfoMap.get(jobId), jobStartExecutionTimestamp, jobStopExecutionTimestamp);
284             mErrorLogger.logError(
285                     AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SPE_UNAVAILABLE_JOB_EXECUTION_START_TIMESTAMP,
286                     AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON);
287             return;
288         }
289 
290         // Compute the execution latency.
291         long executionLatencyMs = jobStopExecutionTimestamp - jobStartExecutionTimestamp;
292 
293         // Update jobStopExecutionTimestamp in storage.
294         editor.putLong(jobStopTimestampKey, jobStopExecutionTimestamp);
295 
296         sReadWriteLock.writeLock().lock();
297         try {
298             if (!editor.commit()) {
299                 // The commitment failure should be rare. It may result in 1 problematic data but
300                 // the impact could be ignored compared to a job's lifecycle.
301                 LogUtil.e(
302                         "Failed to update job Ending Execution Logging Data for Job %s, Job ID ="
303                                 + " %d.",
304                         mJobInfoMap.get(jobId), jobId);
305                 mErrorLogger.logError(
306                         AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SPE_FAIL_TO_COMMIT_JOB_EXECUTION_STOP_TIME,
307                         AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON);
308             }
309         } finally {
310             sReadWriteLock.writeLock().unlock();
311         }
312 
313         // Actually upload the metrics to statsD.
314         logJobStatsHelper(
315                 jobId,
316                 executionLatencyMs,
317                 jobExecutionPeriodMs,
318                 executionResultCode,
319                 possibleStopReason);
320     }
321 
322     /**
323      * Do background job telemetry.
324      *
325      * @param jobId the job ID
326      * @param executionLatencyMs the latency of an execution. Defined as the difference of timestamp
327      *     between end and start of an execution.
328      * @param executionPeriodMs the execution period. Defined as the difference of timestamp between
329      *     current and previous start of an execution. This is only valid for periodical jobs to
330      *     monitor the difference between actual and configured execution period.
331      * @param resultCode the result code of an execution
332      * @param stopReason {@link JobParameters#getStopReason()} if {@link
333      *     JobService#onStopJob(JobParameters)} is invoked. Otherwise, set it to {@link
334      *     JobServiceConstants#UNAVAILABLE_STOP_REASON}.
335      */
336     @VisibleForTesting
logJobStatsHelper( int jobId, long executionLatencyMs, long executionPeriodMs, int resultCode, int stopReason)337     public void logJobStatsHelper(
338             int jobId,
339             long executionLatencyMs,
340             long executionPeriodMs,
341             int resultCode,
342             int stopReason) {
343         if (!shouldLog()) {
344             if (VERBOSE) {
345                 LogUtil.v(
346                         "This background job logging isn't selected for sampling logging, skip...");
347             }
348             return;
349         }
350 
351         // Since the execution period will be logged with unit of minute, it will be converted to 0
352         // if less than MILLISECONDS_PER_MINUTE. The negative period has two scenarios: 1) the first
353         // execution (as -1) or 2) invalid, which will be logged by CEL. As all negative values will
354         // be filtered out in the server's metric, keep the original value of them, to avoid a 0
355         // value due to small negative values.
356         long executionPeriodMinute =
357                 executionPeriodMs >= 0
358                         ? executionPeriodMs / MILLISECONDS_PER_MINUTE
359                         : executionPeriodMs;
360 
361         ExecutionReportedStats stats =
362                 ExecutionReportedStats.builder()
363                         .setJobId(jobId)
364                         .setExecutionLatencyMs(convertLongToInteger(executionLatencyMs))
365                         .setExecutionPeriodMinute(convertLongToInteger(executionPeriodMinute))
366                         .setExecutionResultCode(resultCode)
367                         .setStopReason(stopReason)
368                         // TODO(b/324323522): Populate correct module name.
369                         .setModuleName(EXECUTION_LOGGING_UNKNOWN_MODULE_NAME)
370                         .build();
371         mStatsdLogger.logExecutionReportedStats(stats);
372 
373         if (VERBOSE) {
374             LogUtil.v(
375                     "[Background job execution logging] jobId: %d, executionLatencyInMs: %d,"
376                         + " executionPeriodInMs: %d, resultCode: %d, stopReason: %d, moduleName:"
377                         + " %d",
378                     jobId,
379                     executionLatencyMs,
380                     executionPeriodMs,
381                     resultCode,
382                     stopReason,
383                     EXECUTION_LOGGING_UNKNOWN_MODULE_NAME);
384         }
385     }
386 
387     /**
388      * Compute execution data such as latency and period then store the data in persistent so that
389      * we can compute the job stats later. Store start job timestamp and execution period into the
390      * storage.
391      *
392      * @param jobId the job id
393      * @param startJobTimestamp the timestamp when {@link JobService#onStartJob(JobParameters)} is
394      *     invoked.
395      */
396     @VisibleForTesting
persistJobExecutionData(int jobId, long startJobTimestamp)397     public void persistJobExecutionData(int jobId, long startJobTimestamp) {
398         SharedPreferences sharedPreferences =
399                 mContext.getSharedPreferences(SHARED_PREFS_BACKGROUND_JOBS, Context.MODE_PRIVATE);
400 
401         String jobStartTimestampKey = getJobStartTimestampKey(jobId);
402         String executionPeriodKey = getExecutionPeriodKey(jobId);
403         String jobStopTimestampKey = getJobStopTimestampKey(jobId);
404 
405         // When onStartJob() is invoked, the data stored in the shared preference is for previous
406         // execution.
407         //
408         // JobService is scheduled as JobStatus in JobScheduler infra. Before a JobStatus instance
409         // is pushed to pendingJobQueue, it checks a few criteria like whether a same JobStatus is
410         // ready to execute, not pending, not running, etc. To determine if two JobStatus instances
411         // are same, it checks jobId, callingUid (the package that schedules the job). Therefore,
412         // there won't have two pending/running job instances with a same jobId. For more details,
413         // please check source code of JobScheduler.
414         long previousJobStartTimestamp;
415         long previousJobStopTimestamp;
416         long previousExecutionPeriod;
417 
418         sReadWriteLock.readLock().lock();
419         try {
420             previousJobStartTimestamp =
421                     sharedPreferences.getLong(
422                             jobStartTimestampKey, UNAVAILABLE_JOB_EXECUTION_START_TIMESTAMP);
423             previousJobStopTimestamp =
424                     sharedPreferences.getLong(
425                             jobStopTimestampKey, UNAVAILABLE_JOB_EXECUTION_STOP_TIMESTAMP);
426             previousExecutionPeriod =
427                     sharedPreferences.getLong(executionPeriodKey, UNAVAILABLE_JOB_EXECUTION_PERIOD);
428         } finally {
429             sReadWriteLock.readLock().unlock();
430         }
431 
432         SharedPreferences.Editor editor = sharedPreferences.edit();
433 
434         // The first execution, pass execution period with UNAVAILABLE_JOB_EXECUTION_PERIOD.
435         if (previousJobStartTimestamp == UNAVAILABLE_JOB_EXECUTION_START_TIMESTAMP) {
436             editor.putLong(executionPeriodKey, UNAVAILABLE_JOB_EXECUTION_PERIOD);
437         } else {
438             // If previousJobStartTimestamp is later than previousJobStopTimestamp, it indicates the
439             // last execution didn't finish with calling jobFinished() or onStopJob(). In this case,
440             // we log as an unknown issue, which may come from system/device.
441             if (previousJobStartTimestamp > previousJobStopTimestamp) {
442                 logJobStatsHelper(
443                         jobId,
444                         UNAVAILABLE_JOB_LATENCY,
445                         previousExecutionPeriod,
446                         AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__HALTED_FOR_UNKNOWN_REASON,
447                         UNAVAILABLE_STOP_REASON);
448             }
449 
450             // Compute execution period if there has been multiple executions.
451             // Define the execution period = difference of the timestamp of two consecutive
452             // invocations of onStartJob().
453             long executionPeriodInMs = startJobTimestamp - previousJobStartTimestamp;
454             if (executionPeriodInMs < 0) {
455                 LogUtil.e(
456                         "Invalid execution period = %d! Start time for current execution should be"
457                                 + " later than previous execution!",
458                         executionPeriodInMs);
459                 mErrorLogger.logError(
460                         AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SPE_INVALID_EXECUTION_PERIOD,
461                         AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON);
462             }
463 
464             // Store the execution period into shared preference.
465             editor.putLong(executionPeriodKey, executionPeriodInMs);
466         }
467         // Store current JobStartTimestamp into shared preference.
468         editor.putLong(jobStartTimestampKey, startJobTimestamp);
469 
470         sReadWriteLock.writeLock().lock();
471         try {
472             if (!editor.commit()) {
473                 // The commitment failure should be rare. It may result in 1 problematic data but
474                 // the impact could be ignored compared to a job's lifecycle.
475                 LogUtil.e(
476                         "Failed to update onStartJob() Logging Data for Job %s, Job ID = %d",
477                         mJobInfoMap.get(jobId), jobId);
478                 mErrorLogger.logError(
479                         AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SPE_FAIL_TO_COMMIT_JOB_EXECUTION_START_TIME,
480                         AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON);
481             }
482         } finally {
483             sReadWriteLock.writeLock().unlock();
484         }
485     }
486 
487     @VisibleForTesting
getJobStartTimestampKey(int jobId)488     static String getJobStartTimestampKey(int jobId) {
489         return jobId + JobServiceConstants.SHARED_PREFS_START_TIMESTAMP_SUFFIX;
490     }
491 
492     @VisibleForTesting
getJobStopTimestampKey(int jobId)493     static String getJobStopTimestampKey(int jobId) {
494         return jobId + JobServiceConstants.SHARED_PREFS_STOP_TIMESTAMP_SUFFIX;
495     }
496 
497     @VisibleForTesting
getExecutionPeriodKey(int jobId)498     static String getExecutionPeriodKey(int jobId) {
499         return jobId + JobServiceConstants.SHARED_PREFS_EXEC_PERIOD_SUFFIX;
500     }
501 
502     // Convert a long value to an integer.
503     //
504     // Used to convert a time period in long-format but needs to be logged with integer-format.
505     // Generally, a time period should always be a positive integer with a proper design of its
506     // unit.
507     //
508     // Defensively use this method to avoid any Exception.
509     @VisibleForTesting
convertLongToInteger(long longVal)510     static int convertLongToInteger(long longVal) {
511         int intValue;
512 
513         // The given time period should always be in the range of positive integer. Defensively
514         // handle overflow values to avoid potential Exceptions.
515         if (longVal <= Integer.MIN_VALUE) {
516             intValue = Integer.MIN_VALUE;
517         } else if (longVal >= Integer.MAX_VALUE) {
518             intValue = Integer.MAX_VALUE;
519         } else {
520             intValue = (int) longVal;
521         }
522 
523         return intValue;
524     }
525 
526     // Make a random draw to determine if a logging event should be uploaded t0 the logging server.
527     @VisibleForTesting
shouldLog()528     boolean shouldLog() {
529         int loggingRatio = mFlags.getBackgroundJobSamplingLoggingRate();
530 
531         return sRandom.nextInt(MAX_PERCENTAGE) < loggingRatio;
532     }
533 }
534