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