1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15 package com.android.systemui; 16 17 import android.app.Notification; 18 import android.app.NotificationManager; 19 import android.content.Context; 20 import android.os.Bundle; 21 import android.os.UserHandle; 22 import android.service.notification.StatusBarNotification; 23 import android.util.ArrayMap; 24 import android.util.ArraySet; 25 import android.util.Log; 26 import android.util.Slog; 27 import android.util.SparseArray; 28 29 import com.android.internal.messages.nano.SystemMessageProto; 30 31 import java.util.Arrays; 32 33 /** 34 * Foreground service controller, a/k/a Dianne's Dungeon. 35 */ 36 public class ForegroundServiceControllerImpl 37 implements ForegroundServiceController { 38 39 // shelf life of foreground services before they go bad 40 public static final long FG_SERVICE_GRACE_MILLIS = 5000; 41 42 private static final String TAG = "FgServiceController"; 43 private static final boolean DBG = false; 44 45 private final Context mContext; 46 private final SparseArray<UserServices> mUserServices = new SparseArray<>(); 47 private final Object mMutex = new Object(); 48 49 public ForegroundServiceControllerImpl(Context context) { 50 mContext = context; 51 } 52 53 @Override 54 public boolean isDungeonNeededForUser(int userId) { 55 synchronized (mMutex) { 56 final UserServices services = mUserServices.get(userId); 57 if (services == null) return false; 58 return services.isDungeonNeeded(); 59 } 60 } 61 62 @Override 63 public boolean isSystemAlertWarningNeeded(int userId, String pkg) { 64 synchronized (mMutex) { 65 final UserServices services = mUserServices.get(userId); 66 if (services == null) return false; 67 return services.getStandardLayoutKey(pkg) == null; 68 } 69 } 70 71 @Override 72 public String getStandardLayoutKey(int userId, String pkg) { 73 synchronized (mMutex) { 74 final UserServices services = mUserServices.get(userId); 75 if (services == null) return null; 76 return services.getStandardLayoutKey(pkg); 77 } 78 } 79 80 @Override 81 public ArraySet<Integer> getAppOps(int userId, String pkg) { 82 synchronized (mMutex) { 83 final UserServices services = mUserServices.get(userId); 84 if (services == null) { 85 return null; 86 } 87 return services.getFeatures(pkg); 88 } 89 } 90 91 @Override 92 public void onAppOpChanged(int code, int uid, String packageName, boolean active) { 93 int userId = UserHandle.getUserId(uid); 94 synchronized (mMutex) { 95 UserServices userServices = mUserServices.get(userId); 96 if (userServices == null) { 97 userServices = new UserServices(); 98 mUserServices.put(userId, userServices); 99 } 100 if (active) { 101 userServices.addOp(packageName, code); 102 } else { 103 userServices.removeOp(packageName, code); 104 } 105 } 106 } 107 108 @Override 109 public void addNotification(StatusBarNotification sbn, int importance) { 110 updateNotification(sbn, importance); 111 } 112 113 @Override 114 public boolean removeNotification(StatusBarNotification sbn) { 115 synchronized (mMutex) { 116 final UserServices userServices = mUserServices.get(sbn.getUserId()); 117 if (userServices == null) { 118 if (DBG) { 119 Log.w(TAG, String.format( 120 "user %d with no known notifications got removeNotification for %s", 121 sbn.getUserId(), sbn)); 122 } 123 return false; 124 } 125 if (isDungeonNotification(sbn)) { 126 // if you remove the dungeon entirely, we take that to mean there are 127 // no running services 128 userServices.setRunningServices(null, 0); 129 return true; 130 } else { 131 // this is safe to call on any notification, not just FLAG_FOREGROUND_SERVICE 132 return userServices.removeNotification(sbn.getPackageName(), sbn.getKey()); 133 } 134 } 135 } 136 137 @Override 138 public void updateNotification(StatusBarNotification sbn, int newImportance) { 139 synchronized (mMutex) { 140 UserServices userServices = mUserServices.get(sbn.getUserId()); 141 if (userServices == null) { 142 userServices = new UserServices(); 143 mUserServices.put(sbn.getUserId(), userServices); 144 } 145 146 if (isDungeonNotification(sbn)) { 147 final Bundle extras = sbn.getNotification().extras; 148 if (extras != null) { 149 final String[] svcs = extras.getStringArray(Notification.EXTRA_FOREGROUND_APPS); 150 userServices.setRunningServices(svcs, sbn.getNotification().when); 151 } 152 } else { 153 userServices.removeNotification(sbn.getPackageName(), sbn.getKey()); 154 if (0 != (sbn.getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE)) { 155 if (newImportance > NotificationManager.IMPORTANCE_MIN) { 156 userServices.addImportantNotification(sbn.getPackageName(), sbn.getKey()); 157 } 158 final Notification.Builder builder = Notification.Builder.recoverBuilder( 159 mContext, sbn.getNotification()); 160 if (builder.usesStandardHeader()) { 161 userServices.addStandardLayoutNotification( 162 sbn.getPackageName(), sbn.getKey()); 163 } 164 } 165 } 166 } 167 } 168 169 @Override 170 public boolean isDungeonNotification(StatusBarNotification sbn) { 171 return sbn.getId() == SystemMessageProto.SystemMessage.NOTE_FOREGROUND_SERVICES 172 && sbn.getTag() == null 173 && sbn.getPackageName().equals("android"); 174 } 175 176 @Override 177 public boolean isSystemAlertNotification(StatusBarNotification sbn) { 178 return sbn.getPackageName().equals("android") 179 && sbn.getTag() != null 180 && sbn.getTag().contains("AlertWindowNotification"); 181 } 182 183 /** 184 * Struct to track relevant packages and notifications for a userid's foreground services. 185 */ 186 private static class UserServices { 187 private String[] mRunning = null; 188 private long mServiceStartTime = 0; 189 // package -> sufficiently important posted notification keys 190 private ArrayMap<String, ArraySet<String>> mImportantNotifications = new ArrayMap<>(1); 191 // package -> standard layout posted notification keys 192 private ArrayMap<String, ArraySet<String>> mStandardLayoutNotifications = new ArrayMap<>(1); 193 194 // package -> app ops 195 private ArrayMap<String, ArraySet<Integer>> mAppOps = new ArrayMap<>(1); 196 197 public void setRunningServices(String[] pkgs, long serviceStartTime) { 198 mRunning = pkgs != null ? Arrays.copyOf(pkgs, pkgs.length) : null; 199 mServiceStartTime = serviceStartTime; 200 } 201 202 public void addOp(String pkg, int op) { 203 if (mAppOps.get(pkg) == null) { 204 mAppOps.put(pkg, new ArraySet<>(3)); 205 } 206 mAppOps.get(pkg).add(op); 207 } 208 209 public boolean removeOp(String pkg, int op) { 210 final boolean found; 211 final ArraySet<Integer> keys = mAppOps.get(pkg); 212 if (keys == null) { 213 found = false; 214 } else { 215 found = keys.remove(op); 216 if (keys.size() == 0) { 217 mAppOps.remove(pkg); 218 } 219 } 220 return found; 221 } 222 223 public void addImportantNotification(String pkg, String key) { 224 addNotification(mImportantNotifications, pkg, key); 225 } 226 227 public boolean removeImportantNotification(String pkg, String key) { 228 return removeNotification(mImportantNotifications, pkg, key); 229 } 230 231 public void addStandardLayoutNotification(String pkg, String key) { 232 addNotification(mStandardLayoutNotifications, pkg, key); 233 } 234 235 public boolean removeStandardLayoutNotification(String pkg, String key) { 236 return removeNotification(mStandardLayoutNotifications, pkg, key); 237 } 238 239 public boolean removeNotification(String pkg, String key) { 240 boolean removed = false; 241 removed |= removeImportantNotification(pkg, key); 242 removed |= removeStandardLayoutNotification(pkg, key); 243 return removed; 244 } 245 246 public void addNotification(ArrayMap<String, ArraySet<String>> map, String pkg, 247 String key) { 248 if (map.get(pkg) == null) { 249 map.put(pkg, new ArraySet<>()); 250 } 251 map.get(pkg).add(key); 252 } 253 254 public boolean removeNotification(ArrayMap<String, ArraySet<String>> map, 255 String pkg, String key) { 256 final boolean found; 257 final ArraySet<String> keys = map.get(pkg); 258 if (keys == null) { 259 found = false; 260 } else { 261 found = keys.remove(key); 262 if (keys.size() == 0) { 263 map.remove(pkg); 264 } 265 } 266 return found; 267 } 268 269 public boolean isDungeonNeeded() { 270 if (mRunning != null 271 && System.currentTimeMillis() - mServiceStartTime >= FG_SERVICE_GRACE_MILLIS) { 272 273 for (String pkg : mRunning) { 274 final ArraySet<String> set = mImportantNotifications.get(pkg); 275 if (set == null || set.size() == 0) { 276 return true; 277 } 278 } 279 } 280 return false; 281 } 282 283 public ArraySet<Integer> getFeatures(String pkg) { 284 return mAppOps.get(pkg); 285 } 286 287 public String getStandardLayoutKey(String pkg) { 288 final ArraySet<String> set = mStandardLayoutNotifications.get(pkg); 289 if (set == null || set.size() == 0) { 290 return null; 291 } 292 return set.valueAt(0); 293 } 294 295 @Override 296 public String toString() { 297 return "UserServices{" + 298 "mRunning=" + Arrays.toString(mRunning) + 299 ", mServiceStartTime=" + mServiceStartTime + 300 ", mImportantNotifications=" + mImportantNotifications + 301 ", mStandardLayoutNotifications=" + mStandardLayoutNotifications + 302 '}'; 303 } 304 } 305 } 306