• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /**
2  * Copyright (C) 2018 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 android.ext.services.notification;
17 
18 import static android.app.NotificationManager.IMPORTANCE_MIN;
19 
20 import android.app.AlarmManager;
21 import android.app.PendingIntent;
22 import android.content.BroadcastReceiver;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.IntentFilter;
26 import android.ext.services.notification.NotificationCategorizer.Category;
27 import android.net.Uri;
28 import android.util.ArraySet;
29 import android.util.Log;
30 
31 import java.util.Set;
32 
33 public class AgingHelper {
34     private final static String TAG = "AgingHelper";
35     private final boolean DEBUG = false;
36 
37     private static final String AGING_ACTION = AgingHelper.class.getSimpleName() + ".EVALUATE";
38     private static final int REQUEST_CODE_AGING = 1;
39     private static final String AGING_SCHEME = "aging";
40     private static final String EXTRA_KEY = "key";
41     private static final String EXTRA_CATEGORY = "category";
42 
43     private static final int HOUR_MS = 1000 * 60 * 60;
44     private static final int TWO_HOURS_MS = 2 * HOUR_MS;
45 
46     private Context mContext;
47     private NotificationCategorizer mNotificationCategorizer;
48     private AlarmManager mAm;
49     private Callback mCallback;
50 
51     // The set of keys we've scheduled alarms for
52     private Set<String> mAging = new ArraySet<>();
53 
AgingHelper(Context context, NotificationCategorizer categorizer, Callback callback)54     public AgingHelper(Context context, NotificationCategorizer categorizer, Callback callback) {
55         mNotificationCategorizer = categorizer;
56         mContext = context;
57         mAm = mContext.getSystemService(AlarmManager.class);
58         mCallback = callback;
59 
60         IntentFilter filter = new IntentFilter(AGING_ACTION);
61         filter.addDataScheme(AGING_SCHEME);
62         mContext.registerReceiver(mBroadcastReceiver, filter);
63     }
64 
65     // NAS lifecycle methods
66 
onNotificationSeen(NotificationEntry entry)67     public void onNotificationSeen(NotificationEntry entry) {
68         // user has strong opinions about this notification. we can't down rank it, so don't bother.
69         if (entry.getChannel().hasUserSetImportance()) {
70             return;
71         }
72 
73         @Category int category = mNotificationCategorizer.getCategory(entry);
74 
75         // already very low
76         if (category == NotificationCategorizer.CATEGORY_MIN) {
77             return;
78         }
79 
80         if (entry.hasSeen()) {
81             if (category == NotificationCategorizer.CATEGORY_ONGOING
82                     || category > NotificationCategorizer.CATEGORY_REMINDER) {
83                 scheduleAging(entry.getSbn().getKey(), category, TWO_HOURS_MS);
84             } else {
85                 scheduleAging(entry.getSbn().getKey(), category, HOUR_MS);
86             }
87 
88             mAging.add(entry.getSbn().getKey());
89         }
90     }
91 
onNotificationPosted(NotificationEntry entry)92     public void onNotificationPosted(NotificationEntry entry) {
93         cancelAging(entry.getSbn().getKey());
94     }
95 
onNotificationRemoved(String key)96     public void onNotificationRemoved(String key) {
97         cancelAging(key);
98     }
99 
onDestroy()100     public void onDestroy() {
101         mContext.unregisterReceiver(mBroadcastReceiver);
102     }
103 
104     // Aging
105 
scheduleAging(String key, @Category int category, long duration)106     private void scheduleAging(String key, @Category int category, long duration) {
107         if (mAging.contains(key)) {
108             // already scheduled. Don't reset aging just because the user saw the noti again.
109             return;
110         }
111         final PendingIntent pi = createPendingIntent(key, category);
112         long time = System.currentTimeMillis() + duration;
113         if (DEBUG) Log.d(TAG, "Scheduling evaluate for " + key + " in ms: " + duration);
114         mAm.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, pi);
115     }
116 
cancelAging(String key)117     private void cancelAging(String key) {
118         final PendingIntent pi = createPendingIntent(key);
119         mAm.cancel(pi);
120         mAging.remove(key);
121     }
122 
createBaseIntent(String key)123     private Intent createBaseIntent(String key) {
124         return new Intent(AGING_ACTION)
125                 .setData(new Uri.Builder().scheme(AGING_SCHEME).appendPath(key).build());
126     }
127 
createAgingIntent(String key, @Category int category)128     private Intent createAgingIntent(String key, @Category int category) {
129         Intent intent = createBaseIntent(key);
130         intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
131                 .putExtra(EXTRA_CATEGORY, category)
132                 .putExtra(EXTRA_KEY, key);
133         return intent;
134     }
135 
createPendingIntent(String key, @Category int category)136     private PendingIntent createPendingIntent(String key, @Category int category) {
137         return PendingIntent.getBroadcast(mContext,
138                 REQUEST_CODE_AGING,
139                 createAgingIntent(key, category),
140                 PendingIntent.FLAG_UPDATE_CURRENT);
141     }
142 
createPendingIntent(String key)143     private PendingIntent createPendingIntent(String key) {
144         return PendingIntent.getBroadcast(mContext,
145                 REQUEST_CODE_AGING,
146                 createBaseIntent(key),
147                 PendingIntent.FLAG_UPDATE_CURRENT);
148     }
149 
demote(String key, @Category int category)150     private void demote(String key, @Category int category) {
151         int newImportance = IMPORTANCE_MIN;
152         // TODO: Change "aged" importance based on category
153         mCallback.sendAdjustment(key, newImportance);
154     }
155 
156     protected interface Callback {
sendAdjustment(String key, int newImportance)157         void sendAdjustment(String key, int newImportance);
158     }
159 
160     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
161         @Override
162         public void onReceive(Context context, Intent intent) {
163             if (DEBUG) {
164                 Log.d(TAG, "Reposting notification");
165             }
166             if (AGING_ACTION.equals(intent.getAction())) {
167                 demote(intent.getStringExtra(EXTRA_KEY), intent.getIntExtra(EXTRA_CATEGORY,
168                         NotificationCategorizer.CATEGORY_EVERYTHING_ELSE));
169             }
170         }
171     };
172 }
173