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.permissioncontroller.permission.service
18 
19 import android.app.job.JobInfo
20 import android.app.job.JobParameters
21 import android.app.job.JobScheduler
22 import android.app.job.JobService
23 import android.content.ComponentName
24 import android.content.Context
25 import android.provider.DeviceConfig
26 import com.android.permissioncontroller.Constants
27 import com.android.permissioncontroller.DumpableLog
28 import com.android.permissioncontroller.permission.utils.Utils
29 import java.util.concurrent.TimeUnit
30 import kotlinx.coroutines.Dispatchers
31 import kotlinx.coroutines.GlobalScope
32 import kotlinx.coroutines.Job
33 import kotlinx.coroutines.launch
34 
35 /** A job to clean up old permission events. */
36 class PermissionEventCleanupJobService : JobService() {
37 
38     companion object {
39         const val LOG_TAG = "PermissionEventCleanupJobService"
40         val DEFAULT_CLEAR_OLD_EVENTS_CHECK_FREQUENCY = TimeUnit.DAYS.toMillis(1)
41 
scheduleOldDataCleanupIfNecessarynull42         fun scheduleOldDataCleanupIfNecessary(context: Context, jobScheduler: JobScheduler) {
43             if (isNewJobScheduleRequired(jobScheduler)) {
44                 val jobInfo =
45                     JobInfo.Builder(
46                             Constants.OLD_PERMISSION_EVENT_CLEANUP_JOB_ID,
47                             ComponentName(context, PermissionEventCleanupJobService::class.java)
48                         )
49                         .setPeriodic(getClearOldEventsCheckFrequencyMs())
50                         // persist this job across boots
51                         .setPersisted(true)
52                         .build()
53                 val status = jobScheduler.schedule(jobInfo)
54                 if (status != JobScheduler.RESULT_SUCCESS) {
55                     DumpableLog.e(LOG_TAG, "Could not schedule job: $status")
56                 }
57             }
58         }
59 
60         /**
61          * Returns whether a new job needs to be scheduled. A persisted job is used to keep the
62          * schedule across boots, but that job needs to be scheduled a first time and whenever the
63          * check frequency changes.
64          */
isNewJobScheduleRequirednull65         private fun isNewJobScheduleRequired(jobScheduler: JobScheduler): Boolean {
66             var scheduleNewJob = false
67             val existingJob: JobInfo? =
68                 jobScheduler.getPendingJob(Constants.OLD_PERMISSION_EVENT_CLEANUP_JOB_ID)
69             when {
70                 existingJob == null -> {
71                     DumpableLog.i(LOG_TAG, "No existing job, scheduling a new one")
72                     scheduleNewJob = true
73                 }
74                 existingJob.intervalMillis != getClearOldEventsCheckFrequencyMs() -> {
75                     DumpableLog.i(LOG_TAG, "Interval frequency has changed, updating job")
76                     scheduleNewJob = true
77                 }
78                 else -> {
79                     DumpableLog.i(LOG_TAG, "Job already scheduled.")
80                 }
81             }
82             return scheduleNewJob
83         }
84 
getClearOldEventsCheckFrequencyMsnull85         private fun getClearOldEventsCheckFrequencyMs() =
86             DeviceConfig.getLong(
87                 DeviceConfig.NAMESPACE_PERMISSIONS,
88                 Utils.PROPERTY_PERMISSION_EVENTS_CHECK_OLD_FREQUENCY_MILLIS,
89                 DEFAULT_CLEAR_OLD_EVENTS_CHECK_FREQUENCY
90             )
91     }
92 
93     var job: Job? = null
94     var jobStartTime: Long = -1L
95 
96     override fun onStartJob(params: JobParameters?): Boolean {
97         DumpableLog.i(LOG_TAG, "onStartJob")
98         val storages = PermissionEventStorageImpls.getInstance()
99         if (storages.isEmpty()) {
100             return false
101         }
102         jobStartTime = System.currentTimeMillis()
103         job =
104             GlobalScope.launch(Dispatchers.IO) {
105                 for (storage in storages) {
106                     val success = storage.removeOldData()
107                     if (!success) {
108                         DumpableLog.e(LOG_TAG, "Failed to remove old data for $storage")
109                     }
110                 }
111                 jobFinished(params, false)
112             }
113         return true
114     }
115 
onStopJobnull116     override fun onStopJob(params: JobParameters?): Boolean {
117         DumpableLog.w(LOG_TAG, "onStopJob after ${System.currentTimeMillis() - jobStartTime}ms")
118         job?.cancel()
119         return true
120     }
121 }
122