/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the
 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package com.android.systemui;

import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
import android.os.Bundle;
import android.os.UserHandle;
import android.service.notification.StatusBarNotification;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;

import com.android.internal.messages.nano.SystemMessageProto;

import java.util.Arrays;

Foreground service controller, a/k/a Dianne's Dungeon.
/** * Foreground service controller, a/k/a Dianne's Dungeon. */
public class ForegroundServiceControllerImpl implements ForegroundServiceController { // shelf life of foreground services before they go bad public static final long FG_SERVICE_GRACE_MILLIS = 5000; private static final String TAG = "FgServiceController"; private static final boolean DBG = false; private final Context mContext; private final SparseArray<UserServices> mUserServices = new SparseArray<>(); private final Object mMutex = new Object(); public ForegroundServiceControllerImpl(Context context) { mContext = context; } @Override public boolean isDungeonNeededForUser(int userId) { synchronized (mMutex) { final UserServices services = mUserServices.get(userId); if (services == null) return false; return services.isDungeonNeeded(); } } @Override public boolean isSystemAlertWarningNeeded(int userId, String pkg) { synchronized (mMutex) { final UserServices services = mUserServices.get(userId); if (services == null) return false; return services.getStandardLayoutKey(pkg) == null; } } @Override public String getStandardLayoutKey(int userId, String pkg) { synchronized (mMutex) { final UserServices services = mUserServices.get(userId); if (services == null) return null; return services.getStandardLayoutKey(pkg); } } @Override public ArraySet<Integer> getAppOps(int userId, String pkg) { synchronized (mMutex) { final UserServices services = mUserServices.get(userId); if (services == null) { return null; } return services.getFeatures(pkg); } } @Override public void onAppOpChanged(int code, int uid, String packageName, boolean active) { int userId = UserHandle.getUserId(uid); synchronized (mMutex) { UserServices userServices = mUserServices.get(userId); if (userServices == null) { userServices = new UserServices(); mUserServices.put(userId, userServices); } if (active) { userServices.addOp(packageName, code); } else { userServices.removeOp(packageName, code); } } } @Override public void addNotification(StatusBarNotification sbn, int importance) { updateNotification(sbn, importance); } @Override public boolean removeNotification(StatusBarNotification sbn) { synchronized (mMutex) { final UserServices userServices = mUserServices.get(sbn.getUserId()); if (userServices == null) { if (DBG) { Log.w(TAG, String.format( "user %d with no known notifications got removeNotification for %s", sbn.getUserId(), sbn)); } return false; } if (isDungeonNotification(sbn)) { // if you remove the dungeon entirely, we take that to mean there are // no running services userServices.setRunningServices(null, 0); return true; } else { // this is safe to call on any notification, not just FLAG_FOREGROUND_SERVICE return userServices.removeNotification(sbn.getPackageName(), sbn.getKey()); } } } @Override public void updateNotification(StatusBarNotification sbn, int newImportance) { synchronized (mMutex) { UserServices userServices = mUserServices.get(sbn.getUserId()); if (userServices == null) { userServices = new UserServices(); mUserServices.put(sbn.getUserId(), userServices); } if (isDungeonNotification(sbn)) { final Bundle extras = sbn.getNotification().extras; if (extras != null) { final String[] svcs = extras.getStringArray(Notification.EXTRA_FOREGROUND_APPS); userServices.setRunningServices(svcs, sbn.getNotification().when); } } else { userServices.removeNotification(sbn.getPackageName(), sbn.getKey()); if (0 != (sbn.getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE)) { if (newImportance > NotificationManager.IMPORTANCE_MIN) { userServices.addImportantNotification(sbn.getPackageName(), sbn.getKey()); } final Notification.Builder builder = Notification.Builder.recoverBuilder( mContext, sbn.getNotification()); if (builder.usesStandardHeader()) { userServices.addStandardLayoutNotification( sbn.getPackageName(), sbn.getKey()); } } } } } @Override public boolean isDungeonNotification(StatusBarNotification sbn) { return sbn.getId() == SystemMessageProto.SystemMessage.NOTE_FOREGROUND_SERVICES && sbn.getTag() == null && sbn.getPackageName().equals("android"); } @Override public boolean isSystemAlertNotification(StatusBarNotification sbn) { return sbn.getPackageName().equals("android") && sbn.getTag() != null && sbn.getTag().contains("AlertWindowNotification"); }
Struct to track relevant packages and notifications for a userid's foreground services.
/** * Struct to track relevant packages and notifications for a userid's foreground services. */
private static class UserServices { private String[] mRunning = null; private long mServiceStartTime = 0; // package -> sufficiently important posted notification keys private ArrayMap<String, ArraySet<String>> mImportantNotifications = new ArrayMap<>(1); // package -> standard layout posted notification keys private ArrayMap<String, ArraySet<String>> mStandardLayoutNotifications = new ArrayMap<>(1); // package -> app ops private ArrayMap<String, ArraySet<Integer>> mAppOps = new ArrayMap<>(1); public void setRunningServices(String[] pkgs, long serviceStartTime) { mRunning = pkgs != null ? Arrays.copyOf(pkgs, pkgs.length) : null; mServiceStartTime = serviceStartTime; } public void addOp(String pkg, int op) { if (mAppOps.get(pkg) == null) { mAppOps.put(pkg, new ArraySet<>(3)); } mAppOps.get(pkg).add(op); } public boolean removeOp(String pkg, int op) { final boolean found; final ArraySet<Integer> keys = mAppOps.get(pkg); if (keys == null) { found = false; } else { found = keys.remove(op); if (keys.size() == 0) { mAppOps.remove(pkg); } } return found; } public void addImportantNotification(String pkg, String key) { addNotification(mImportantNotifications, pkg, key); } public boolean removeImportantNotification(String pkg, String key) { return removeNotification(mImportantNotifications, pkg, key); } public void addStandardLayoutNotification(String pkg, String key) { addNotification(mStandardLayoutNotifications, pkg, key); } public boolean removeStandardLayoutNotification(String pkg, String key) { return removeNotification(mStandardLayoutNotifications, pkg, key); } public boolean removeNotification(String pkg, String key) { boolean removed = false; removed |= removeImportantNotification(pkg, key); removed |= removeStandardLayoutNotification(pkg, key); return removed; } public void addNotification(ArrayMap<String, ArraySet<String>> map, String pkg, String key) { if (map.get(pkg) == null) { map.put(pkg, new ArraySet<>()); } map.get(pkg).add(key); } public boolean removeNotification(ArrayMap<String, ArraySet<String>> map, String pkg, String key) { final boolean found; final ArraySet<String> keys = map.get(pkg); if (keys == null) { found = false; } else { found = keys.remove(key); if (keys.size() == 0) { map.remove(pkg); } } return found; } public boolean isDungeonNeeded() { if (mRunning != null && System.currentTimeMillis() - mServiceStartTime >= FG_SERVICE_GRACE_MILLIS) { for (String pkg : mRunning) { final ArraySet<String> set = mImportantNotifications.get(pkg); if (set == null || set.size() == 0) { return true; } } } return false; } public ArraySet<Integer> getFeatures(String pkg) { return mAppOps.get(pkg); } public String getStandardLayoutKey(String pkg) { final ArraySet<String> set = mStandardLayoutNotifications.get(pkg); if (set == null || set.size() == 0) { return null; } return set.valueAt(0); } @Override public String toString() { return "UserServices{" + "mRunning=" + Arrays.toString(mRunning) + ", mServiceStartTime=" + mServiceStartTime + ", mImportantNotifications=" + mImportantNotifications + ", mStandardLayoutNotifications=" + mStandardLayoutNotifications + '}'; } } }