• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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