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 final TreeSet<Pair<Long, String>> mKeys; 58 TimeToLiveHelper(NotificationManagerPrivate nm, Context context)59 public TimeToLiveHelper(NotificationManagerPrivate nm, Context context) { 60 mContext = context; 61 mNm = nm; 62 mAm = context.getSystemService(AlarmManager.class); 63 mKeys = new TreeSet<>((left, right) -> Long.compare(left.first, right.first)); 64 65 IntentFilter timeoutFilter = new IntentFilter(ACTION); 66 timeoutFilter.addDataScheme(SCHEME_TIMEOUT); 67 mContext.registerReceiver(mNotificationTimeoutReceiver, timeoutFilter, 68 Context.RECEIVER_NOT_EXPORTED); 69 } 70 destroy()71 void destroy() { 72 mContext.unregisterReceiver(mNotificationTimeoutReceiver); 73 } 74 dump(PrintWriter pw, String indent)75 void dump(PrintWriter pw, String indent) { 76 pw.println(indent + "mKeys " + mKeys); 77 } 78 getAlarmPendingIntent(String nextKey, int flags)79 private @NonNull PendingIntent getAlarmPendingIntent(String nextKey, int flags) { 80 flags |= PendingIntent.FLAG_IMMUTABLE; 81 return PendingIntent.getBroadcast(mContext, 82 REQUEST_CODE_TIMEOUT, 83 new Intent(ACTION) 84 .setPackage(PackageManagerService.PLATFORM_PACKAGE_NAME) 85 .setData(new Uri.Builder() 86 .scheme(SCHEME_TIMEOUT) 87 .appendPath(nextKey) 88 .build()) 89 .putExtra(EXTRA_KEY, nextKey) 90 .addFlags(Intent.FLAG_RECEIVER_FOREGROUND), 91 flags); 92 } 93 94 @VisibleForTesting scheduleTimeoutLocked(NotificationRecord record, long currentTime)95 void scheduleTimeoutLocked(NotificationRecord record, long currentTime) { 96 removeMatchingEntry(record.getKey()); 97 98 final long timeoutAfter = currentTime + record.getNotification().getTimeoutAfter(); 99 if (record.getNotification().getTimeoutAfter() > 0) { 100 final Long currentEarliestTime = mKeys.isEmpty() ? null : mKeys.first().first; 101 102 // Maybe replace alarm with an earlier one 103 if (currentEarliestTime == null || timeoutAfter < currentEarliestTime) { 104 if (currentEarliestTime != null) { 105 cancelFirstAlarm(); 106 } 107 mKeys.add(Pair.create(timeoutAfter, record.getKey())); 108 maybeScheduleFirstAlarm(); 109 } else { 110 mKeys.add(Pair.create(timeoutAfter, record.getKey())); 111 } 112 } 113 } 114 115 @VisibleForTesting cancelScheduledTimeoutLocked(NotificationRecord record)116 void cancelScheduledTimeoutLocked(NotificationRecord record) { 117 removeMatchingEntry(record.getKey()); 118 } 119 removeMatchingEntry(String key)120 private void removeMatchingEntry(String key) { 121 if (!mKeys.isEmpty() && key.equals(mKeys.first().second)) { 122 // cancel the first alarm, remove the first entry, maybe schedule the alarm for the new 123 // first entry 124 cancelFirstAlarm(); 125 mKeys.remove(mKeys.first()); 126 maybeScheduleFirstAlarm(); 127 } else { 128 // just remove the entry 129 Pair<Long, String> trackedPair = null; 130 for (Pair<Long, String> entry : mKeys) { 131 if (key.equals(entry.second)) { 132 trackedPair = entry; 133 break; 134 } 135 } 136 if (trackedPair != null) { 137 mKeys.remove(trackedPair); 138 } 139 } 140 } 141 cancelFirstAlarm()142 private void cancelFirstAlarm() { 143 final PendingIntent pi = getAlarmPendingIntent(mKeys.first().second, FLAG_CANCEL_CURRENT); 144 mAm.cancel(pi); 145 } 146 maybeScheduleFirstAlarm()147 private void maybeScheduleFirstAlarm() { 148 if (!mKeys.isEmpty()) { 149 final PendingIntent piNewFirst = getAlarmPendingIntent(mKeys.first().second, 150 FLAG_UPDATE_CURRENT); 151 mAm.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, 152 mKeys.first().first, piNewFirst); 153 } 154 } 155 156 @VisibleForTesting 157 final BroadcastReceiver mNotificationTimeoutReceiver = new BroadcastReceiver() { 158 @Override 159 public void onReceive(Context context, Intent intent) { 160 String action = intent.getAction(); 161 if (action == null) { 162 return; 163 } 164 if (ACTION.equals(action)) { 165 Pair<Long, String> earliest = mKeys.first(); 166 String key = intent.getStringExtra(EXTRA_KEY); 167 if (!earliest.second.equals(key)) { 168 Slog.wtf(TAG, "Alarm triggered but wasn't the earliest we were tracking"); 169 } 170 removeMatchingEntry(key); 171 mNm.timeoutNotification(earliest.second); 172 } 173 } 174 }; 175 } 176