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.server.notification; 18 19 import static android.app.PendingIntent.FLAG_CANCEL_CURRENT; 20 import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; 21 22 import android.annotation.FlaggedApi; 23 import android.annotation.NonNull; 24 import android.app.AlarmManager; 25 import android.app.PendingIntent; 26 import android.content.BroadcastReceiver; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.IntentFilter; 30 import android.net.Uri; 31 import android.os.SystemClock; 32 import android.util.Pair; 33 import android.util.Slog; 34 import com.android.internal.annotations.GuardedBy; 35 import com.android.internal.annotations.VisibleForTesting; 36 import com.android.server.pm.PackageManagerService; 37 38 import java.io.PrintWriter; 39 import java.util.TreeSet; 40 41 /** 42 * Handles canceling notifications when their time to live expires 43 */ 44 @FlaggedApi(Flags.FLAG_ALL_NOTIFS_NEED_TTL) 45 public class TimeToLiveHelper { 46 private static final String TAG = TimeToLiveHelper.class.getSimpleName(); 47 private static final String ACTION = "com.android.server.notification.TimeToLiveHelper"; 48 49 private static final int REQUEST_CODE_TIMEOUT = 1; 50 private static final String SCHEME_TIMEOUT = "timeout"; 51 static final String EXTRA_KEY = "key"; 52 private final Context mContext; 53 private final NotificationManagerPrivate mNm; 54 private final AlarmManager mAm; 55 56 @VisibleForTesting 57 @GuardedBy("mLock") 58 final TreeSet<Pair<Long, String>> mKeys; 59 final Object mLock = new Object(); 60 TimeToLiveHelper(NotificationManagerPrivate nm, Context context)61 public TimeToLiveHelper(NotificationManagerPrivate nm, Context context) { 62 mContext = context; 63 mNm = nm; 64 mAm = context.getSystemService(AlarmManager.class); 65 synchronized (mLock) { 66 mKeys = new TreeSet<>((left, right) -> Long.compare(left.first, right.first)); 67 } 68 69 IntentFilter timeoutFilter = new IntentFilter(ACTION); 70 timeoutFilter.addDataScheme(SCHEME_TIMEOUT); 71 mContext.registerReceiver(mNotificationTimeoutReceiver, timeoutFilter, 72 Context.RECEIVER_NOT_EXPORTED); 73 } 74 destroy()75 void destroy() { 76 mContext.unregisterReceiver(mNotificationTimeoutReceiver); 77 } 78 dump(PrintWriter pw, String indent)79 void dump(PrintWriter pw, String indent) { 80 synchronized (mLock) { 81 pw.println(indent + "mKeys " + mKeys); 82 } 83 } 84 getAlarmPendingIntent(String nextKey, int flags)85 private @NonNull PendingIntent getAlarmPendingIntent(String nextKey, int flags) { 86 flags |= PendingIntent.FLAG_IMMUTABLE; 87 return PendingIntent.getBroadcast(mContext, 88 REQUEST_CODE_TIMEOUT, 89 new Intent(ACTION) 90 .setPackage(PackageManagerService.PLATFORM_PACKAGE_NAME) 91 .setData(new Uri.Builder() 92 .scheme(SCHEME_TIMEOUT) 93 .appendPath(nextKey) 94 .build()) 95 .putExtra(EXTRA_KEY, nextKey) 96 .addFlags(Intent.FLAG_RECEIVER_FOREGROUND), 97 flags); 98 } 99 100 @VisibleForTesting scheduleTimeoutLocked(NotificationRecord record, long currentTime)101 void scheduleTimeoutLocked(NotificationRecord record, long currentTime) { 102 synchronized (mLock) { 103 removeMatchingEntry(record.getKey()); 104 105 final long timeoutAfter = currentTime + record.getNotification().getTimeoutAfter(); 106 if (record.getNotification().getTimeoutAfter() > 0) { 107 final Long currentEarliestTime = mKeys.isEmpty() ? null : mKeys.first().first; 108 109 // Maybe replace alarm with an earlier one 110 if (currentEarliestTime == null || timeoutAfter < currentEarliestTime) { 111 if (currentEarliestTime != null) { 112 cancelFirstAlarm(); 113 } 114 mKeys.add(Pair.create(timeoutAfter, record.getKey())); 115 maybeScheduleFirstAlarm(); 116 } else { 117 mKeys.add(Pair.create(timeoutAfter, record.getKey())); 118 } 119 } 120 } 121 } 122 123 @VisibleForTesting cancelScheduledTimeoutLocked(NotificationRecord record)124 void cancelScheduledTimeoutLocked(NotificationRecord record) { 125 synchronized (mLock) { 126 removeMatchingEntry(record.getKey()); 127 } 128 } 129 130 @GuardedBy("mLock") removeMatchingEntry(String key)131 private void removeMatchingEntry(String key) { 132 if (!mKeys.isEmpty() && key.equals(mKeys.first().second)) { 133 // cancel the first alarm, remove the first entry, maybe schedule the alarm for the new 134 // first entry 135 cancelFirstAlarm(); 136 mKeys.remove(mKeys.first()); 137 maybeScheduleFirstAlarm(); 138 } else { 139 // just remove the entry 140 Pair<Long, String> trackedPair = null; 141 for (Pair<Long, String> entry : mKeys) { 142 if (key.equals(entry.second)) { 143 trackedPair = entry; 144 break; 145 } 146 } 147 if (trackedPair != null) { 148 mKeys.remove(trackedPair); 149 } 150 } 151 } 152 153 @GuardedBy("mLock") cancelFirstAlarm()154 private void cancelFirstAlarm() { 155 final PendingIntent pi = getAlarmPendingIntent(mKeys.first().second, FLAG_CANCEL_CURRENT); 156 mAm.cancel(pi); 157 } 158 159 @GuardedBy("mLock") maybeScheduleFirstAlarm()160 private void maybeScheduleFirstAlarm() { 161 if (!mKeys.isEmpty()) { 162 final PendingIntent piNewFirst = getAlarmPendingIntent(mKeys.first().second, 163 FLAG_UPDATE_CURRENT); 164 mAm.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, 165 mKeys.first().first, piNewFirst); 166 } 167 } 168 169 @VisibleForTesting 170 final BroadcastReceiver mNotificationTimeoutReceiver = new BroadcastReceiver() { 171 @Override 172 public void onReceive(Context context, Intent intent) { 173 String action = intent.getAction(); 174 if (action == null) { 175 return; 176 } 177 if (ACTION.equals(action)) { 178 String timeoutKey = null; 179 synchronized (mLock) { 180 if (!mKeys.isEmpty()) { 181 Pair<Long, String> earliest = mKeys.first(); 182 String key = intent.getStringExtra(EXTRA_KEY); 183 if (!earliest.second.equals(key)) { 184 Slog.wtf(TAG, 185 "Alarm triggered but wasn't the earliest we were tracking"); 186 } 187 removeMatchingEntry(key); 188 timeoutKey = earliest.second; 189 } 190 } 191 if (timeoutKey != null) { 192 mNm.timeoutNotification(timeoutKey); 193 } else { 194 Slog.wtf(TAG, "Alarm triggered but should have been cleaned up already"); 195 } 196 } 197 } 198 }; 199 } 200