1 /**
2  * Copyright (C) 2023 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.notification;
17 
18 import static android.app.job.JobScheduler.RESULT_SUCCESS;
19 
20 import android.app.job.JobInfo;
21 import android.app.job.JobParameters;
22 import android.app.job.JobScheduler;
23 import android.app.job.JobService;
24 import android.content.ComponentName;
25 import android.content.Context;
26 import android.os.CancellationSignal;
27 import android.util.Slog;
28 
29 import com.android.internal.annotations.VisibleForTesting;
30 import com.android.server.LocalServices;
31 
32 import java.time.Duration;
33 import java.time.Instant;
34 import java.time.LocalDate;
35 import java.time.LocalTime;
36 import java.time.ZonedDateTime;
37 import java.time.ZoneId;
38 
39 /**
40  * This service runs everyday at 2am local time to remove expired bitmaps.
41  */
42 public class NotificationBitmapJobService extends JobService {
43     static final String TAG = "NotificationBitmapJob";
44 
45     static final int BASE_JOB_ID = 290381858; // Feature bug id
46 
scheduleJob(Context context)47     static void scheduleJob(Context context) {
48         if (context == null) {
49             return;
50         }
51         try {
52             JobScheduler jobScheduler =
53                     context.getSystemService(JobScheduler.class).forNamespace(TAG);
54 
55             ComponentName component =
56                     new ComponentName(context, NotificationBitmapJobService.class);
57 
58             JobInfo jobInfo = new JobInfo.Builder(BASE_JOB_ID, component)
59                     .setRequiresDeviceIdle(true)
60                     .setMinimumLatency(getRunAfterMs())
61                     .build();
62 
63             final int result = jobScheduler.schedule(jobInfo);
64             if (result != RESULT_SUCCESS) {
65                 Slog.e(TAG, "Failed to schedule bitmap removal job");
66             }
67 
68         } catch (Throwable e) {
69             Slog.wtf(TAG, "Failed bitmap removal job", e);
70         }
71     }
72 
73     /**
74      * @return Milliseconds until the next time the job should run.
75      */
getRunAfterMs()76     private static long getRunAfterMs() {
77         ZoneId zoneId = ZoneId.systemDefault();
78         ZonedDateTime now = Instant.now().atZone(zoneId);
79 
80         LocalDate today = now.toLocalDate();
81         LocalTime twoAM = LocalTime.of(/* hour= */ 2, /* minute= */ 0);
82 
83         ZonedDateTime today2AM = ZonedDateTime.of(today, twoAM, zoneId);
84         ZonedDateTime tomorrow2AM = today2AM.plusDays(1);
85 
86         return getTimeUntilRemoval(now, today2AM, tomorrow2AM);
87     }
88 
89     @VisibleForTesting
getTimeUntilRemoval(ZonedDateTime now, ZonedDateTime today2AM, ZonedDateTime tomorrow2AM)90     static long getTimeUntilRemoval(ZonedDateTime now, ZonedDateTime today2AM,
91                                     ZonedDateTime tomorrow2AM) {
92         if (Duration.between(now, today2AM).isNegative()) {
93             return Duration.between(now, tomorrow2AM).toMillis();
94         }
95         return Duration.between(now, today2AM).toMillis();
96     }
97 
98     @Override
onStartJob(JobParameters params)99     public boolean onStartJob(JobParameters params) {
100         new Thread(() -> {
101             NotificationManagerInternal nmInternal =
102                     LocalServices.getService(NotificationManagerInternal.class);
103             nmInternal.removeBitmaps();
104 
105             // Schedule the next job here, since we cannot use setPeriodic and setMinimumLatency
106             // together when creating JobInfo.
107             scheduleJob(/* context= */ this);
108 
109             jobFinished(params, /* wantsReschedule= */ false);
110         }).start();
111 
112         return true;  // This service continues to run in a separate thread.
113     }
114 
115     @Override
onStopJob(JobParameters params)116     public boolean onStopJob(JobParameters params) {
117         // No need to keep this job alive since the original thread is going to keep running and
118         // call scheduleJob at the end of its execution.
119         return false;
120     }
121 
122     @Override
123     @VisibleForTesting
attachBaseContext(Context base)124     protected void attachBaseContext(Context base) {
125         super.attachBaseContext(base);
126     }
127 }