1/**
2 * Copyright (c) 2015, 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
17package com.android.server.notification;
18
19import static android.provider.Settings.Global.ZEN_MODE_OFF;
20
21import android.app.Notification;
22import android.app.NotificationManager;
23import android.content.ComponentName;
24import android.content.Context;
25import android.media.AudioAttributes;
26import android.media.AudioManager;
27import android.os.Bundle;
28import android.os.UserHandle;
29import android.provider.Settings.Global;
30import android.provider.Settings.Secure;
31import android.service.notification.ZenModeConfig;
32import android.telecom.TelecomManager;
33import android.util.ArrayMap;
34import android.util.Slog;
35
36import com.android.internal.messages.nano.SystemMessageProto;
37import com.android.internal.util.NotificationMessagingUtil;
38
39import java.io.PrintWriter;
40import java.util.Date;
41import java.util.Objects;
42
43public class ZenModeFiltering {
44    private static final String TAG = ZenModeHelper.TAG;
45    private static final boolean DEBUG = ZenModeHelper.DEBUG;
46
47    static final RepeatCallers REPEAT_CALLERS = new RepeatCallers();
48
49    private final Context mContext;
50
51    private ComponentName mDefaultPhoneApp;
52    private final NotificationMessagingUtil mMessagingUtil;
53
54    public ZenModeFiltering(Context context) {
55        mContext = context;
56        mMessagingUtil = new NotificationMessagingUtil(mContext);
57    }
58
59    public ZenModeFiltering(Context context, NotificationMessagingUtil messagingUtil) {
60        mContext = context;
61        mMessagingUtil = messagingUtil;
62    }
63
64    public void dump(PrintWriter pw, String prefix) {
65        pw.print(prefix); pw.print("mDefaultPhoneApp="); pw.println(mDefaultPhoneApp);
66        pw.print(prefix); pw.print("RepeatCallers.mThresholdMinutes=");
67        pw.println(REPEAT_CALLERS.mThresholdMinutes);
68        synchronized (REPEAT_CALLERS) {
69            if (!REPEAT_CALLERS.mCalls.isEmpty()) {
70                pw.print(prefix); pw.println("RepeatCallers.mCalls=");
71                for (int i = 0; i < REPEAT_CALLERS.mCalls.size(); i++) {
72                    pw.print(prefix); pw.print("  ");
73                    pw.print(REPEAT_CALLERS.mCalls.keyAt(i));
74                    pw.print(" at ");
75                    pw.println(ts(REPEAT_CALLERS.mCalls.valueAt(i)));
76                }
77            }
78        }
79    }
80
81    private static String ts(long time) {
82        return new Date(time) + " (" + time + ")";
83    }
84
85    /**
86     * @param extras extras of the notification with EXTRA_PEOPLE populated
87     * @param contactsTimeoutMs timeout in milliseconds to wait for contacts response
88     * @param timeoutAffinity affinity to return when the timeout specified via
89     *                        <code>contactsTimeoutMs</code> is hit
90     */
91    public static boolean matchesCallFilter(Context context, int zen, ZenModeConfig config,
92            UserHandle userHandle, Bundle extras, ValidateNotificationPeople validator,
93            int contactsTimeoutMs, float timeoutAffinity) {
94        if (zen == Global.ZEN_MODE_NO_INTERRUPTIONS) return false; // nothing gets through
95        if (zen == Global.ZEN_MODE_ALARMS) return false; // not an alarm
96        if (zen == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS) {
97            if (config.allowRepeatCallers && REPEAT_CALLERS.isRepeat(context, extras)) {
98                return true;
99            }
100            if (!config.allowCalls) return false; // no other calls get through
101            if (validator != null) {
102                final float contactAffinity = validator.getContactAffinity(userHandle, extras,
103                        contactsTimeoutMs, timeoutAffinity);
104                return audienceMatches(config.allowCallsFrom, contactAffinity);
105            }
106        }
107        return true;
108    }
109
110    private static Bundle extras(NotificationRecord record) {
111        return record != null && record.sbn != null && record.sbn.getNotification() != null
112                ? record.sbn.getNotification().extras : null;
113    }
114
115    protected void recordCall(NotificationRecord record) {
116        REPEAT_CALLERS.recordCall(mContext, extras(record));
117    }
118
119    public boolean shouldIntercept(int zen, ZenModeConfig config, NotificationRecord record) {
120        if (zen == ZEN_MODE_OFF) {
121            return false;
122        }
123        // Make an exception to policy for the notification saying that policy has changed
124        if (NotificationManager.Policy.areAllVisualEffectsSuppressed(config.suppressedVisualEffects)
125                && "android".equals(record.sbn.getPackageName())
126                && SystemMessageProto.SystemMessage.NOTE_ZEN_UPGRADE == record.sbn.getId()) {
127            ZenLog.traceNotIntercepted(record, "systemDndChangedNotification");
128            return false;
129        }
130        switch (zen) {
131            case Global.ZEN_MODE_NO_INTERRUPTIONS:
132                // #notevenalarms
133                ZenLog.traceIntercepted(record, "none");
134                return true;
135            case Global.ZEN_MODE_ALARMS:
136                if (isAlarm(record)) {
137                    // Alarms only
138                    return false;
139                }
140                ZenLog.traceIntercepted(record, "alarmsOnly");
141                return true;
142            case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS:
143                // allow user-prioritized packages through in priority mode
144                if (record.getPackagePriority() == Notification.PRIORITY_MAX) {
145                    ZenLog.traceNotIntercepted(record, "priorityApp");
146                    return false;
147                }
148
149                if (isAlarm(record)) {
150                    if (!config.allowAlarms) {
151                        ZenLog.traceIntercepted(record, "!allowAlarms");
152                        return true;
153                    }
154                    return false;
155                }
156                if (isCall(record)) {
157                    if (config.allowRepeatCallers
158                            && REPEAT_CALLERS.isRepeat(mContext, extras(record))) {
159                        ZenLog.traceNotIntercepted(record, "repeatCaller");
160                        return false;
161                    }
162                    if (!config.allowCalls) {
163                        ZenLog.traceIntercepted(record, "!allowCalls");
164                        return true;
165                    }
166                    return shouldInterceptAudience(config.allowCallsFrom, record);
167                }
168                if (isMessage(record)) {
169                    if (!config.allowMessages) {
170                        ZenLog.traceIntercepted(record, "!allowMessages");
171                        return true;
172                    }
173                    return shouldInterceptAudience(config.allowMessagesFrom, record);
174                }
175                if (isEvent(record)) {
176                    if (!config.allowEvents) {
177                        ZenLog.traceIntercepted(record, "!allowEvents");
178                        return true;
179                    }
180                    return false;
181                }
182                if (isReminder(record)) {
183                    if (!config.allowReminders) {
184                        ZenLog.traceIntercepted(record, "!allowReminders");
185                        return true;
186                    }
187                    return false;
188                }
189                if (isMedia(record)) {
190                    if (!config.allowMedia) {
191                        ZenLog.traceIntercepted(record, "!allowMedia");
192                        return true;
193                    }
194                    return false;
195                }
196                if (isSystem(record)) {
197                    if (!config.allowSystem) {
198                        ZenLog.traceIntercepted(record, "!allowSystem");
199                        return true;
200                    }
201                    return false;
202                }
203                ZenLog.traceIntercepted(record, "!priority");
204                return true;
205            default:
206                return false;
207        }
208    }
209
210    private static boolean shouldInterceptAudience(int source, NotificationRecord record) {
211        if (!audienceMatches(source, record.getContactAffinity())) {
212            ZenLog.traceIntercepted(record, "!audienceMatches");
213            return true;
214        }
215        return false;
216    }
217
218    protected static boolean isAlarm(NotificationRecord record) {
219        return record.isCategory(Notification.CATEGORY_ALARM)
220                || record.isAudioAttributesUsage(AudioAttributes.USAGE_ALARM);
221    }
222
223    private static boolean isEvent(NotificationRecord record) {
224        return record.isCategory(Notification.CATEGORY_EVENT);
225    }
226
227    private static boolean isReminder(NotificationRecord record) {
228        return record.isCategory(Notification.CATEGORY_REMINDER);
229    }
230
231    public boolean isCall(NotificationRecord record) {
232        return record != null && (isDefaultPhoneApp(record.sbn.getPackageName())
233                || record.isCategory(Notification.CATEGORY_CALL));
234    }
235
236    public boolean isMedia(NotificationRecord record) {
237        AudioAttributes aa = record.getAudioAttributes();
238        return aa != null && AudioAttributes.SUPPRESSIBLE_USAGES.get(aa.getUsage()) ==
239                AudioAttributes.SUPPRESSIBLE_MEDIA;
240    }
241
242    public boolean isSystem(NotificationRecord record) {
243        AudioAttributes aa = record.getAudioAttributes();
244        return aa != null && AudioAttributes.SUPPRESSIBLE_USAGES.get(aa.getUsage()) ==
245                AudioAttributes.SUPPRESSIBLE_SYSTEM;
246    }
247
248    private boolean isDefaultPhoneApp(String pkg) {
249        if (mDefaultPhoneApp == null) {
250            final TelecomManager telecomm =
251                    (TelecomManager) mContext.getSystemService(Context.TELECOM_SERVICE);
252            mDefaultPhoneApp = telecomm != null ? telecomm.getDefaultPhoneApp() : null;
253            if (DEBUG) Slog.d(TAG, "Default phone app: " + mDefaultPhoneApp);
254        }
255        return pkg != null && mDefaultPhoneApp != null
256                && pkg.equals(mDefaultPhoneApp.getPackageName());
257    }
258
259    protected boolean isMessage(NotificationRecord record) {
260        return mMessagingUtil.isMessaging(record.sbn);
261    }
262
263    private static boolean audienceMatches(int source, float contactAffinity) {
264        switch (source) {
265            case ZenModeConfig.SOURCE_ANYONE:
266                return true;
267            case ZenModeConfig.SOURCE_CONTACT:
268                return contactAffinity >= ValidateNotificationPeople.VALID_CONTACT;
269            case ZenModeConfig.SOURCE_STAR:
270                return contactAffinity >= ValidateNotificationPeople.STARRED_CONTACT;
271            default:
272                Slog.w(TAG, "Encountered unknown source: " + source);
273                return true;
274        }
275    }
276
277    private static class RepeatCallers {
278        // Person : time
279        private final ArrayMap<String, Long> mCalls = new ArrayMap<>();
280        private int mThresholdMinutes;
281
282        private synchronized void recordCall(Context context, Bundle extras) {
283            setThresholdMinutes(context);
284            if (mThresholdMinutes <= 0 || extras == null) return;
285            final String peopleString = peopleString(extras);
286            if (peopleString == null) return;
287            final long now = System.currentTimeMillis();
288            cleanUp(mCalls, now);
289            mCalls.put(peopleString, now);
290        }
291
292        private synchronized boolean isRepeat(Context context, Bundle extras) {
293            setThresholdMinutes(context);
294            if (mThresholdMinutes <= 0 || extras == null) return false;
295            final String peopleString = peopleString(extras);
296            if (peopleString == null) return false;
297            final long now = System.currentTimeMillis();
298            cleanUp(mCalls, now);
299            return mCalls.containsKey(peopleString);
300        }
301
302        private synchronized void cleanUp(ArrayMap<String, Long> calls, long now) {
303            final int N = calls.size();
304            for (int i = N - 1; i >= 0; i--) {
305                final long time = mCalls.valueAt(i);
306                if (time > now || (now - time) > mThresholdMinutes * 1000 * 60) {
307                    calls.removeAt(i);
308                }
309            }
310        }
311
312        private void setThresholdMinutes(Context context) {
313            if (mThresholdMinutes <= 0) {
314                mThresholdMinutes = context.getResources().getInteger(com.android.internal.R.integer
315                        .config_zen_repeat_callers_threshold);
316            }
317        }
318
319        private static String peopleString(Bundle extras) {
320            final String[] extraPeople = ValidateNotificationPeople.getExtraPeople(extras);
321            if (extraPeople == null || extraPeople.length == 0) return null;
322            final StringBuilder sb = new StringBuilder();
323            for (int i = 0; i < extraPeople.length; i++) {
324                String extraPerson = extraPeople[i];
325                if (extraPerson == null) continue;
326                extraPerson = extraPerson.trim();
327                if (extraPerson.isEmpty()) continue;
328                if (sb.length() > 0) {
329                    sb.append('|');
330                }
331                sb.append(extraPerson);
332            }
333            return sb.length() == 0 ? null : sb.toString();
334        }
335    }
336
337}
338