blob: 1fa925e5ad1f0eba7b5253223208022a3e7422b1 [file] [log] [blame]
/*
* 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.
*/
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.
*/
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 +
'}';
}
}
}