1 /*
2  * Copyright (C) 2019 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 package com.android.server.usage;
17 
18 import android.app.job.JobInfo;
19 import android.app.job.JobParameters;
20 import android.app.job.JobScheduler;
21 import android.app.job.JobService;
22 import android.app.usage.UsageStatsManagerInternal;
23 import android.content.ComponentName;
24 import android.content.Context;
25 import android.os.AsyncTask;
26 import android.os.PersistableBundle;
27 
28 import com.android.server.LocalServices;
29 
30 import java.util.concurrent.TimeUnit;
31 
32 /**
33  * JobService used to do any work for UsageStats while the device is idle.
34  */
35 public class UsageStatsIdleService extends JobService {
36 
37     /**
38      * Base job ID for the pruning job - must be unique within the system server uid.
39      */
40     private static final int PRUNE_JOB_ID = 546357475;
41     /**
42      * Job ID for the update mappings job - must be unique within the system server uid.
43      * Incrementing PRUNE_JOB_ID by 21475 (MAX_USER_ID) to ensure there is no overlap in job ids.
44      */
45     private static final int UPDATE_MAPPINGS_JOB_ID = 546378950;
46 
47     private static final String USER_ID_KEY = "user_id";
48 
scheduleJob(Context context, int userId)49     static void scheduleJob(Context context, int userId) {
50         final int userJobId = PRUNE_JOB_ID + userId; // unique job id per user
51         final ComponentName component = new ComponentName(context.getPackageName(),
52                 UsageStatsIdleService.class.getName());
53         final PersistableBundle bundle = new PersistableBundle();
54         bundle.putInt(USER_ID_KEY, userId);
55         final JobInfo pruneJob = new JobInfo.Builder(userJobId, component)
56                 .setRequiresDeviceIdle(true)
57                 .setExtras(bundle)
58                 .setPersisted(true)
59                 .build();
60 
61         scheduleJobInternal(context, pruneJob, userJobId);
62     }
63 
scheduleUpdateMappingsJob(Context context)64     static void scheduleUpdateMappingsJob(Context context) {
65         final ComponentName component = new ComponentName(context.getPackageName(),
66                 UsageStatsIdleService.class.getName());
67         final JobInfo updateMappingsJob = new JobInfo.Builder(UPDATE_MAPPINGS_JOB_ID, component)
68                 .setPersisted(true)
69                 .setMinimumLatency(TimeUnit.DAYS.toMillis(1))
70                 .setOverrideDeadline(TimeUnit.DAYS.toMillis(2))
71                 .build();
72 
73         scheduleJobInternal(context, updateMappingsJob, UPDATE_MAPPINGS_JOB_ID);
74     }
75 
scheduleJobInternal(Context context, JobInfo pruneJob, int jobId)76     private static void scheduleJobInternal(Context context, JobInfo pruneJob, int jobId) {
77         final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
78         final JobInfo pendingPruneJob = jobScheduler.getPendingJob(jobId);
79         // only schedule a new prune job if one doesn't exist already for this user
80         if (!pruneJob.equals(pendingPruneJob)) {
81             jobScheduler.cancel(jobId); // cancel any previously scheduled prune job
82             jobScheduler.schedule(pruneJob);
83         }
84     }
85 
cancelJob(Context context, int userId)86     static void cancelJob(Context context, int userId) {
87         cancelJobInternal(context, PRUNE_JOB_ID + userId);
88     }
89 
cancelUpdateMappingsJob(Context context)90     static void cancelUpdateMappingsJob(Context context) {
91         cancelJobInternal(context, UPDATE_MAPPINGS_JOB_ID);
92     }
93 
cancelJobInternal(Context context, int jobId)94     private static void cancelJobInternal(Context context, int jobId) {
95         final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
96         if (jobScheduler != null) {
97             jobScheduler.cancel(jobId);
98         }
99     }
100 
101     @Override
onStartJob(JobParameters params)102     public boolean onStartJob(JobParameters params) {
103         final PersistableBundle bundle = params.getExtras();
104         final int userId = bundle.getInt(USER_ID_KEY, -1);
105         if (userId == -1 && params.getJobId() != UPDATE_MAPPINGS_JOB_ID) {
106             return false;
107         }
108 
109         AsyncTask.execute(() -> {
110             final UsageStatsManagerInternal usageStatsManagerInternal = LocalServices.getService(
111                     UsageStatsManagerInternal.class);
112             if (params.getJobId() == UPDATE_MAPPINGS_JOB_ID) {
113                 final boolean jobFinished = usageStatsManagerInternal.updatePackageMappingsData();
114                 jobFinished(params, !jobFinished); // reschedule if data was not updated
115             } else {
116                 final boolean jobFinished =
117                         usageStatsManagerInternal.pruneUninstalledPackagesData(userId);
118                 jobFinished(params, !jobFinished); // reschedule if data was not pruned
119             }
120         });
121         return true;
122     }
123 
124     @Override
onStopJob(JobParameters params)125     public boolean onStopJob(JobParameters params) {
126         // Since the pruning job isn't a heavy job, we don't want to cancel it's execution midway.
127         return false;
128     }
129 }
130