1/*
2 * Copyright (C) 2014 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 android.support.v4.app;
18
19import android.app.AppOpsManager;
20import android.app.Notification;
21import android.app.NotificationManager;
22import android.app.Service;
23import android.content.ComponentName;
24import android.content.Context;
25import android.content.Intent;
26import android.content.ServiceConnection;
27import android.content.pm.ApplicationInfo;
28import android.content.pm.PackageManager;
29import android.content.pm.ResolveInfo;
30import android.os.Build;
31import android.os.Bundle;
32import android.os.DeadObjectException;
33import android.os.Handler;
34import android.os.HandlerThread;
35import android.os.IBinder;
36import android.os.Message;
37import android.os.RemoteException;
38import android.provider.Settings;
39import android.support.annotation.GuardedBy;
40import android.util.Log;
41
42import java.lang.reflect.Field;
43import java.lang.reflect.InvocationTargetException;
44import java.lang.reflect.Method;
45import java.util.HashMap;
46import java.util.HashSet;
47import java.util.Iterator;
48import java.util.LinkedList;
49import java.util.List;
50import java.util.Map;
51import java.util.Set;
52
53/**
54 * Compatibility library for NotificationManager with fallbacks for older platforms.
55 *
56 * <p>To use this class, call the static function {@link #from} to get a
57 * {@link NotificationManagerCompat} object, and then call one of its
58 * methods to post or cancel notifications.
59 */
60public final class NotificationManagerCompat {
61    private static final String TAG = "NotifManCompat";
62    private static final String CHECK_OP_NO_THROW = "checkOpNoThrow";
63    private static final String OP_POST_NOTIFICATION = "OP_POST_NOTIFICATION";
64
65    /**
66     * Notification extras key: if set to true, the posted notification should use
67     * the side channel for delivery instead of using notification manager.
68     */
69    public static final String EXTRA_USE_SIDE_CHANNEL =
70            NotificationCompatJellybean.EXTRA_USE_SIDE_CHANNEL;
71
72    /**
73     * Intent action to register for on a service to receive side channel
74     * notifications. The listening service must be in the same package as an enabled
75     * {@link android.service.notification.NotificationListenerService}.
76     */
77    public static final String ACTION_BIND_SIDE_CHANNEL =
78            "android.support.BIND_NOTIFICATION_SIDE_CHANNEL";
79
80    /**
81     * Maximum sdk build version which needs support for side channeled notifications.
82     * Currently the only needed use is for side channeling group children before KITKAT_WATCH.
83     */
84    static final int MAX_SIDE_CHANNEL_SDK_VERSION = 19;
85
86    /** Base time delay for a side channel listener queue retry. */
87    private static final int SIDE_CHANNEL_RETRY_BASE_INTERVAL_MS = 1000;
88    /** Maximum retries for a side channel listener before dropping tasks. */
89    private static final int SIDE_CHANNEL_RETRY_MAX_COUNT = 6;
90    /** Hidden field Settings.Secure.ENABLED_NOTIFICATION_LISTENERS */
91    private static final String SETTING_ENABLED_NOTIFICATION_LISTENERS =
92            "enabled_notification_listeners";
93
94    /** Cache of enabled notification listener components */
95    private static final Object sEnabledNotificationListenersLock = new Object();
96    @GuardedBy("sEnabledNotificationListenersLock")
97    private static String sEnabledNotificationListeners;
98    @GuardedBy("sEnabledNotificationListenersLock")
99    private static Set<String> sEnabledNotificationListenerPackages = new HashSet<String>();
100
101    private final Context mContext;
102    private final NotificationManager mNotificationManager;
103    /** Lock for mutable static fields */
104    private static final Object sLock = new Object();
105    @GuardedBy("sLock")
106    private static SideChannelManager sSideChannelManager;
107
108    /**
109     * Value signifying that the user has not expressed an importance.
110     *
111     * This value is for persisting preferences, and should never be associated with
112     * an actual notification.
113     */
114    public static final int IMPORTANCE_UNSPECIFIED = -1000;
115
116    /**
117     * A notification with no importance: shows nowhere, is blocked.
118     */
119    public static final int IMPORTANCE_NONE = 0;
120
121    /**
122     * Min notification importance: only shows in the shade, below the fold.
123     */
124    public static final int IMPORTANCE_MIN = 1;
125
126    /**
127     * Low notification importance: shows everywhere, but is not intrusive.
128     */
129    public static final int IMPORTANCE_LOW = 2;
130
131    /**
132     * Default notification importance: shows everywhere, allowed to makes noise,
133     * but does not visually intrude.
134     */
135    public static final int IMPORTANCE_DEFAULT = 3;
136
137    /**
138     * Higher notification importance: shows everywhere, allowed to makes noise and peek.
139     */
140    public static final int IMPORTANCE_HIGH = 4;
141
142    /**
143     * Highest notification importance: shows everywhere, allowed to makes noise, peek, and
144     * use full screen intents.
145     */
146    public static final int IMPORTANCE_MAX = 5;
147
148    /** Get a {@link NotificationManagerCompat} instance for a provided context. */
149    public static NotificationManagerCompat from(Context context) {
150        return new NotificationManagerCompat(context);
151    }
152
153    private NotificationManagerCompat(Context context) {
154        mContext = context;
155        mNotificationManager = (NotificationManager) mContext.getSystemService(
156                Context.NOTIFICATION_SERVICE);
157    }
158
159    /**
160     * Cancel a previously shown notification.
161     * @param id the ID of the notification
162     */
163    public void cancel(int id) {
164        cancel(null, id);
165    }
166
167    /**
168     * Cancel a previously shown notification.
169     * @param tag the string identifier of the notification.
170     * @param id the ID of the notification
171     */
172    public void cancel(String tag, int id) {
173        mNotificationManager.cancel(tag, id);
174        if (Build.VERSION.SDK_INT <= MAX_SIDE_CHANNEL_SDK_VERSION) {
175            pushSideChannelQueue(new CancelTask(mContext.getPackageName(), id, tag));
176        }
177    }
178
179    /** Cancel all previously shown notifications. */
180    public void cancelAll() {
181        mNotificationManager.cancelAll();
182        if (Build.VERSION.SDK_INT <= MAX_SIDE_CHANNEL_SDK_VERSION) {
183            pushSideChannelQueue(new CancelTask(mContext.getPackageName()));
184        }
185    }
186
187    /**
188     * Post a notification to be shown in the status bar, stream, etc.
189     * @param id the ID of the notification
190     * @param notification the notification to post to the system
191     */
192    public void notify(int id, Notification notification) {
193        notify(null, id, notification);
194    }
195
196    /**
197     * Post a notification to be shown in the status bar, stream, etc.
198     * @param tag the string identifier for a notification. Can be {@code null}.
199     * @param id the ID of the notification. The pair (tag, id) must be unique within your app.
200     * @param notification the notification to post to the system
201    */
202    public void notify(String tag, int id, Notification notification) {
203        if (useSideChannelForNotification(notification)) {
204            pushSideChannelQueue(new NotifyTask(mContext.getPackageName(), id, tag, notification));
205            // Cancel this notification in notification manager if it just transitioned to being
206            // side channelled.
207            mNotificationManager.cancel(tag, id);
208        } else {
209            mNotificationManager.notify(tag, id, notification);
210        }
211    }
212
213    /**
214     * Returns whether notifications from the calling package are not blocked.
215     */
216    public boolean areNotificationsEnabled() {
217        if (Build.VERSION.SDK_INT >= 24) {
218            return mNotificationManager.areNotificationsEnabled();
219        } else if (Build.VERSION.SDK_INT >= 19) {
220            AppOpsManager appOps =
221                    (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
222            ApplicationInfo appInfo = mContext.getApplicationInfo();
223            String pkg = mContext.getApplicationContext().getPackageName();
224            int uid = appInfo.uid;
225            try {
226                Class<?> appOpsClass = Class.forName(AppOpsManager.class.getName());
227                Method checkOpNoThrowMethod = appOpsClass.getMethod(CHECK_OP_NO_THROW, Integer.TYPE,
228                        Integer.TYPE, String.class);
229                Field opPostNotificationValue = appOpsClass.getDeclaredField(OP_POST_NOTIFICATION);
230                int value = (int) opPostNotificationValue.get(Integer.class);
231                return ((int) checkOpNoThrowMethod.invoke(appOps, value, uid, pkg)
232                        == AppOpsManager.MODE_ALLOWED);
233            } catch (ClassNotFoundException | NoSuchMethodException | NoSuchFieldException
234                    | InvocationTargetException | IllegalAccessException | RuntimeException e) {
235                return true;
236            }
237        } else {
238            return true;
239        }
240    }
241
242    /**
243     * Returns the user specified importance for notifications from the calling package.
244     *
245     * @return An importance level, such as {@link #IMPORTANCE_DEFAULT}.
246     */
247    public int getImportance() {
248        if (Build.VERSION.SDK_INT >= 24) {
249            return mNotificationManager.getImportance();
250        } else {
251            return IMPORTANCE_UNSPECIFIED;
252        }
253    }
254
255    /**
256     * Get the set of packages that have an enabled notification listener component within them.
257     */
258    public static Set<String> getEnabledListenerPackages(Context context) {
259        final String enabledNotificationListeners = Settings.Secure.getString(
260                context.getContentResolver(),
261                SETTING_ENABLED_NOTIFICATION_LISTENERS);
262        synchronized (sEnabledNotificationListenersLock) {
263            // Parse the string again if it is different from the last time this method was called.
264            if (enabledNotificationListeners != null
265                    && !enabledNotificationListeners.equals(sEnabledNotificationListeners)) {
266                final String[] components = enabledNotificationListeners.split(":");
267                Set<String> packageNames = new HashSet<String>(components.length);
268                for (String component : components) {
269                    ComponentName componentName = ComponentName.unflattenFromString(component);
270                    if (componentName != null) {
271                        packageNames.add(componentName.getPackageName());
272                    }
273                }
274                sEnabledNotificationListenerPackages = packageNames;
275                sEnabledNotificationListeners = enabledNotificationListeners;
276            }
277            return sEnabledNotificationListenerPackages;
278        }
279    }
280
281    /**
282     * Returns true if this notification should use the side channel for delivery.
283     */
284    private static boolean useSideChannelForNotification(Notification notification) {
285        Bundle extras = NotificationCompat.getExtras(notification);
286        return extras != null && extras.getBoolean(EXTRA_USE_SIDE_CHANNEL);
287    }
288
289    /**
290     * Push a notification task for distribution to notification side channels.
291     */
292    private void pushSideChannelQueue(Task task) {
293        synchronized (sLock) {
294            if (sSideChannelManager == null) {
295                sSideChannelManager = new SideChannelManager(mContext.getApplicationContext());
296            }
297            sSideChannelManager.queueTask(task);
298        }
299    }
300
301    /**
302     * Helper class to manage a queue of pending tasks to send to notification side channel
303     * listeners.
304     */
305    private static class SideChannelManager implements Handler.Callback, ServiceConnection {
306        private static final int MSG_QUEUE_TASK = 0;
307        private static final int MSG_SERVICE_CONNECTED = 1;
308        private static final int MSG_SERVICE_DISCONNECTED = 2;
309        private static final int MSG_RETRY_LISTENER_QUEUE = 3;
310
311        private final Context mContext;
312        private final HandlerThread mHandlerThread;
313        private final Handler mHandler;
314        private final Map<ComponentName, ListenerRecord> mRecordMap =
315                new HashMap<ComponentName, ListenerRecord>();
316        private Set<String> mCachedEnabledPackages = new HashSet<String>();
317
318        public SideChannelManager(Context context) {
319            mContext = context;
320            mHandlerThread = new HandlerThread("NotificationManagerCompat");
321            mHandlerThread.start();
322            mHandler = new Handler(mHandlerThread.getLooper(), this);
323        }
324
325        /**
326         * Queue a new task to be sent to all listeners. This function can be called
327         * from any thread.
328         */
329        public void queueTask(Task task) {
330            mHandler.obtainMessage(MSG_QUEUE_TASK, task).sendToTarget();
331        }
332
333        @Override
334        public boolean handleMessage(Message msg) {
335            switch (msg.what) {
336                case MSG_QUEUE_TASK:
337                    handleQueueTask((Task) msg.obj);
338                    return true;
339                case MSG_SERVICE_CONNECTED:
340                    ServiceConnectedEvent event = (ServiceConnectedEvent) msg.obj;
341                    handleServiceConnected(event.componentName, event.iBinder);
342                    return true;
343                case MSG_SERVICE_DISCONNECTED:
344                    handleServiceDisconnected((ComponentName) msg.obj);
345                    return true;
346                case MSG_RETRY_LISTENER_QUEUE:
347                    handleRetryListenerQueue((ComponentName) msg.obj);
348                    return true;
349            }
350            return false;
351        }
352
353        private void handleQueueTask(Task task) {
354            updateListenerMap();
355            for (ListenerRecord record : mRecordMap.values()) {
356                record.taskQueue.add(task);
357                processListenerQueue(record);
358            }
359        }
360
361        private void handleServiceConnected(ComponentName componentName, IBinder iBinder) {
362            ListenerRecord record = mRecordMap.get(componentName);
363            if (record != null) {
364                record.service = INotificationSideChannel.Stub.asInterface(iBinder);
365                record.retryCount = 0;
366                processListenerQueue(record);
367            }
368        }
369
370        private void handleServiceDisconnected(ComponentName componentName) {
371            ListenerRecord record = mRecordMap.get(componentName);
372            if (record != null) {
373                ensureServiceUnbound(record);
374            }
375        }
376
377        private void handleRetryListenerQueue(ComponentName componentName) {
378            ListenerRecord record = mRecordMap.get(componentName);
379            if (record != null) {
380                processListenerQueue(record);
381            }
382        }
383
384        @Override
385        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
386            if (Log.isLoggable(TAG, Log.DEBUG)) {
387                Log.d(TAG, "Connected to service " + componentName);
388            }
389            mHandler.obtainMessage(MSG_SERVICE_CONNECTED,
390                    new ServiceConnectedEvent(componentName, iBinder))
391                    .sendToTarget();
392        }
393
394        @Override
395        public void onServiceDisconnected(ComponentName componentName) {
396            if (Log.isLoggable(TAG, Log.DEBUG)) {
397                Log.d(TAG, "Disconnected from service " + componentName);
398            }
399            mHandler.obtainMessage(MSG_SERVICE_DISCONNECTED, componentName).sendToTarget();
400        }
401
402        /**
403         * Check the current list of enabled listener packages and update the records map
404         * accordingly.
405         */
406        private void updateListenerMap() {
407            Set<String> enabledPackages = getEnabledListenerPackages(mContext);
408            if (enabledPackages.equals(mCachedEnabledPackages)) {
409                // Short-circuit when the list of enabled packages has not changed.
410                return;
411            }
412            mCachedEnabledPackages = enabledPackages;
413            List<ResolveInfo> resolveInfos = mContext.getPackageManager().queryIntentServices(
414                    new Intent().setAction(ACTION_BIND_SIDE_CHANNEL), PackageManager.GET_SERVICES);
415            Set<ComponentName> enabledComponents = new HashSet<ComponentName>();
416            for (ResolveInfo resolveInfo : resolveInfos) {
417                if (!enabledPackages.contains(resolveInfo.serviceInfo.packageName)) {
418                    continue;
419                }
420                ComponentName componentName = new ComponentName(
421                        resolveInfo.serviceInfo.packageName, resolveInfo.serviceInfo.name);
422                if (resolveInfo.serviceInfo.permission != null) {
423                    Log.w(TAG, "Permission present on component " + componentName
424                            + ", not adding listener record.");
425                    continue;
426                }
427                enabledComponents.add(componentName);
428            }
429            // Ensure all enabled components have a record in the listener map.
430            for (ComponentName componentName : enabledComponents) {
431                if (!mRecordMap.containsKey(componentName)) {
432                    if (Log.isLoggable(TAG, Log.DEBUG)) {
433                        Log.d(TAG, "Adding listener record for " + componentName);
434                    }
435                    mRecordMap.put(componentName, new ListenerRecord(componentName));
436                }
437            }
438            // Remove listener records that are no longer for enabled components.
439            Iterator<Map.Entry<ComponentName, ListenerRecord>> it =
440                    mRecordMap.entrySet().iterator();
441            while (it.hasNext()) {
442                Map.Entry<ComponentName, ListenerRecord> entry = it.next();
443                if (!enabledComponents.contains(entry.getKey())) {
444                    if (Log.isLoggable(TAG, Log.DEBUG)) {
445                        Log.d(TAG, "Removing listener record for " + entry.getKey());
446                    }
447                    ensureServiceUnbound(entry.getValue());
448                    it.remove();
449                }
450            }
451        }
452
453        /**
454         * Ensure we are already attempting to bind to a service, or start a new binding if not.
455         * @return Whether the service bind attempt was successful.
456         */
457        private boolean ensureServiceBound(ListenerRecord record) {
458            if (record.bound) {
459                return true;
460            }
461            Intent intent = new Intent(ACTION_BIND_SIDE_CHANNEL).setComponent(record.componentName);
462            record.bound = mContext.bindService(intent, this, Service.BIND_AUTO_CREATE
463                    | Service.BIND_WAIVE_PRIORITY);
464            if (record.bound) {
465                record.retryCount = 0;
466            } else {
467                Log.w(TAG, "Unable to bind to listener " + record.componentName);
468                mContext.unbindService(this);
469            }
470            return record.bound;
471        }
472
473        /**
474         * Ensure we have unbound from a service.
475         */
476        private void ensureServiceUnbound(ListenerRecord record) {
477            if (record.bound) {
478                mContext.unbindService(this);
479                record.bound = false;
480            }
481            record.service = null;
482        }
483
484        /**
485         * Schedule a delayed retry to communicate with a listener service.
486         * After a maximum number of attempts (with exponential back-off), start
487         * dropping pending tasks for this listener.
488         */
489        private void scheduleListenerRetry(ListenerRecord record) {
490            if (mHandler.hasMessages(MSG_RETRY_LISTENER_QUEUE, record.componentName)) {
491                return;
492            }
493            record.retryCount++;
494            if (record.retryCount > SIDE_CHANNEL_RETRY_MAX_COUNT) {
495                Log.w(TAG, "Giving up on delivering " + record.taskQueue.size() + " tasks to "
496                        + record.componentName + " after " + record.retryCount + " retries");
497                record.taskQueue.clear();
498                return;
499            }
500            int delayMs = SIDE_CHANNEL_RETRY_BASE_INTERVAL_MS * (1 << (record.retryCount - 1));
501            if (Log.isLoggable(TAG, Log.DEBUG)) {
502                Log.d(TAG, "Scheduling retry for " + delayMs + " ms");
503            }
504            Message msg = mHandler.obtainMessage(MSG_RETRY_LISTENER_QUEUE, record.componentName);
505            mHandler.sendMessageDelayed(msg, delayMs);
506        }
507
508        /**
509         * Perform a processing step for a listener. First check the bind state, then attempt
510         * to flush the task queue, and if an error is encountered, schedule a retry.
511         */
512        private void processListenerQueue(ListenerRecord record) {
513            if (Log.isLoggable(TAG, Log.DEBUG)) {
514                Log.d(TAG, "Processing component " + record.componentName + ", "
515                        + record.taskQueue.size() + " queued tasks");
516            }
517            if (record.taskQueue.isEmpty()) {
518                return;
519            }
520            if (!ensureServiceBound(record) || record.service == null) {
521                // Ensure bind has started and that a service interface is ready to use.
522                scheduleListenerRetry(record);
523                return;
524            }
525            // Attempt to flush all items in the task queue.
526            while (true) {
527                Task task = record.taskQueue.peek();
528                if (task == null) {
529                    break;
530                }
531                try {
532                    if (Log.isLoggable(TAG, Log.DEBUG)) {
533                        Log.d(TAG, "Sending task " + task);
534                    }
535                    task.send(record.service);
536                    record.taskQueue.remove();
537                } catch (DeadObjectException e) {
538                    if (Log.isLoggable(TAG, Log.DEBUG)) {
539                        Log.d(TAG, "Remote service has died: " + record.componentName);
540                    }
541                    break;
542                } catch (RemoteException e) {
543                    Log.w(TAG, "RemoteException communicating with " + record.componentName, e);
544                    break;
545                }
546            }
547            if (!record.taskQueue.isEmpty()) {
548                // Some tasks were not sent, meaning an error was encountered, schedule a retry.
549                scheduleListenerRetry(record);
550            }
551        }
552
553        /** A per-side-channel-service listener state record */
554        private static class ListenerRecord {
555            public final ComponentName componentName;
556            /** Whether the service is currently bound to. */
557            public boolean bound = false;
558            /** The service stub provided by onServiceConnected */
559            public INotificationSideChannel service;
560            /** Queue of pending tasks to send to this listener service */
561            public LinkedList<Task> taskQueue = new LinkedList<Task>();
562            /** Number of retries attempted while connecting to this listener service */
563            public int retryCount = 0;
564
565            public ListenerRecord(ComponentName componentName) {
566                this.componentName = componentName;
567            }
568        }
569    }
570
571    private static class ServiceConnectedEvent {
572        final ComponentName componentName;
573        final IBinder iBinder;
574
575        ServiceConnectedEvent(ComponentName componentName,
576                final IBinder iBinder) {
577            this.componentName = componentName;
578            this.iBinder = iBinder;
579        }
580    }
581
582    private interface Task {
583        void send(INotificationSideChannel service) throws RemoteException;
584    }
585
586    private static class NotifyTask implements Task {
587        final String packageName;
588        final int id;
589        final String tag;
590        final Notification notif;
591
592        NotifyTask(String packageName, int id, String tag, Notification notif) {
593            this.packageName = packageName;
594            this.id = id;
595            this.tag = tag;
596            this.notif = notif;
597        }
598
599        @Override
600        public void send(INotificationSideChannel service) throws RemoteException {
601            service.notify(packageName, id, tag, notif);
602        }
603
604        @Override
605        public String toString() {
606            StringBuilder sb = new StringBuilder("NotifyTask[");
607            sb.append("packageName:").append(packageName);
608            sb.append(", id:").append(id);
609            sb.append(", tag:").append(tag);
610            sb.append("]");
611            return sb.toString();
612        }
613    }
614
615    private static class CancelTask implements Task {
616        final String packageName;
617        final int id;
618        final String tag;
619        final boolean all;
620
621        CancelTask(String packageName) {
622            this.packageName = packageName;
623            this.id = 0;
624            this.tag = null;
625            this.all = true;
626        }
627
628        CancelTask(String packageName, int id, String tag) {
629            this.packageName = packageName;
630            this.id = id;
631            this.tag = tag;
632            this.all = false;
633        }
634
635        @Override
636        public void send(INotificationSideChannel service) throws RemoteException {
637            if (all) {
638                service.cancelAll(packageName);
639            } else {
640                service.cancel(packageName, id, tag);
641            }
642        }
643
644        @Override
645        public String toString() {
646            StringBuilder sb = new StringBuilder("CancelTask[");
647            sb.append("packageName:").append(packageName);
648            sb.append(", id:").append(id);
649            sb.append(", tag:").append(tag);
650            sb.append(", all:").append(all);
651            sb.append("]");
652            return sb.toString();
653        }
654    }
655}
656