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
15package com.android.systemui;
16
17import android.app.Notification;
18import android.app.NotificationManager;
19import android.content.Context;
20import android.os.Bundle;
21import android.service.notification.StatusBarNotification;
22import android.util.ArrayMap;
23import android.util.ArraySet;
24import android.util.Log;
25import android.util.SparseArray;
26
27import com.android.internal.annotations.VisibleForTesting;
28import com.android.internal.messages.nano.SystemMessageProto;
29
30import java.util.Arrays;
31
32/**
33 * Foreground service controller, a/k/a Dianne's Dungeon.
34 */
35public class ForegroundServiceControllerImpl
36        implements ForegroundServiceController {
37    private static final String TAG = "FgServiceController";
38    private static final boolean DBG = false;
39
40    private final SparseArray<UserServices> mUserServices = new SparseArray<>();
41    private final Object mMutex = new Object();
42
43    public ForegroundServiceControllerImpl(Context context) {
44    }
45
46    @Override
47    public boolean isDungeonNeededForUser(int userId) {
48        synchronized (mMutex) {
49            final UserServices services = mUserServices.get(userId);
50            if (services == null) return false;
51            return services.isDungeonNeeded();
52        }
53    }
54
55    @Override
56    public void addNotification(StatusBarNotification sbn, int importance) {
57        updateNotification(sbn, importance);
58    }
59
60    @Override
61    public boolean removeNotification(StatusBarNotification sbn) {
62        synchronized (mMutex) {
63            final UserServices userServices = mUserServices.get(sbn.getUserId());
64            if (userServices == null) {
65                if (DBG) {
66                    Log.w(TAG, String.format(
67                            "user %d with no known notifications got removeNotification for %s",
68                            sbn.getUserId(), sbn));
69                }
70                return false;
71            }
72            if (isDungeonNotification(sbn)) {
73                // if you remove the dungeon entirely, we take that to mean there are
74                // no running services
75                userServices.setRunningServices(null);
76                return true;
77            } else {
78                // this is safe to call on any notification, not just FLAG_FOREGROUND_SERVICE
79                return userServices.removeNotification(sbn.getPackageName(), sbn.getKey());
80            }
81        }
82    }
83
84    @Override
85    public void updateNotification(StatusBarNotification sbn, int newImportance) {
86        synchronized (mMutex) {
87            UserServices userServices = mUserServices.get(sbn.getUserId());
88            if (userServices == null) {
89                userServices = new UserServices();
90                mUserServices.put(sbn.getUserId(), userServices);
91            }
92
93            if (isDungeonNotification(sbn)) {
94                final Bundle extras = sbn.getNotification().extras;
95                if (extras != null) {
96                    final String[] svcs = extras.getStringArray(Notification.EXTRA_FOREGROUND_APPS);
97                    userServices.setRunningServices(svcs); // null ok
98                }
99            } else {
100                userServices.removeNotification(sbn.getPackageName(), sbn.getKey());
101                if (0 != (sbn.getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE)
102                        && newImportance > NotificationManager.IMPORTANCE_MIN) {
103                    userServices.addNotification(sbn.getPackageName(), sbn.getKey());
104                }
105            }
106        }
107    }
108
109    @Override
110    public boolean isDungeonNotification(StatusBarNotification sbn) {
111        return sbn.getId() == SystemMessageProto.SystemMessage.NOTE_FOREGROUND_SERVICES
112                && sbn.getTag() == null
113                && sbn.getPackageName().equals("android");
114    }
115
116    /**
117     * Struct to track relevant packages and notifications for a userid's foreground services.
118     */
119    private static class UserServices {
120        private String[] mRunning = null;
121        private ArrayMap<String, ArraySet<String>> mNotifications = new ArrayMap<>(1);
122        public void setRunningServices(String[] pkgs) {
123            mRunning = pkgs != null ? Arrays.copyOf(pkgs, pkgs.length) : null;
124        }
125        public void addNotification(String pkg, String key) {
126            if (mNotifications.get(pkg) == null) {
127                mNotifications.put(pkg, new ArraySet<String>());
128            }
129            mNotifications.get(pkg).add(key);
130        }
131        public boolean removeNotification(String pkg, String key) {
132            final boolean found;
133            final ArraySet<String> keys = mNotifications.get(pkg);
134            if (keys == null) {
135                found = false;
136            } else {
137                found = keys.remove(key);
138                if (keys.size() == 0) {
139                    mNotifications.remove(pkg);
140                }
141            }
142            return found;
143        }
144        public boolean isDungeonNeeded() {
145            if (mRunning != null) {
146                for (String pkg : mRunning) {
147                    final ArraySet<String> set = mNotifications.get(pkg);
148                    if (set == null || set.size() == 0) {
149                        return true;
150                    }
151                }
152            }
153            return false;
154        }
155    }
156}
157