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