• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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