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 }