1 /*
<lambda>null2  * Copyright (C) 2021 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 package com.android.bedstead.nene.notifications
17 
18 import android.annotation.SuppressLint
19 import android.app.NotificationManager
20 import android.content.ComponentName
21 import android.os.Build
22 import com.android.bedstead.nene.TestApis
23 import com.android.bedstead.nene.exceptions.AdbException
24 import com.android.bedstead.nene.exceptions.NeneException
25 import com.android.bedstead.nene.packages.ComponentReference
26 import com.android.bedstead.permissions.CommonPermissions.MANAGE_NOTIFICATION_LISTENERS
27 import com.android.bedstead.nene.users.UserReference
28 import com.android.bedstead.nene.utils.ShellCommand
29 import com.android.bedstead.nene.utils.Tags
30 import com.android.bedstead.nene.utils.Versions
31 import java.util.Collections
32 import java.util.concurrent.ConcurrentHashMap
33 
34 /** Helper methods related to notifications.  */
35 object Notifications {
36 
37     private val mRegisteredListeners =
38         Collections.newSetFromMap(ConcurrentHashMap<NotificationListener, Boolean>())
39     private var mListenerAccessIsGranted = false
40 
41     /**
42      * Creates a [NotificationListener].
43      *
44      *
45      * This is required before interacting with notifications in any way. It is recommended that
46      * you do this in a try() block so that the [NotificationListener] closes when you are
47      * finished with it.
48      */
49     fun createListener(): NotificationListener {
50         if (Tags.hasTag(Tags.USES_DEVICESTATE) && !Tags.hasTag(Tags.USES_NOTIFICATIONS)) {
51             throw NeneException(
52                 "Tests which use Notifications must be annotated @NotificationsTest"
53             )
54         }
55         val notificationListener = NotificationListener(this)
56         mRegisteredListeners.add(notificationListener)
57         initListenerIfRequired()
58         return notificationListener
59     }
60 
61     fun removeListener(listener: NotificationListener) {
62         mRegisteredListeners.remove(listener)
63         teardownListenerIfRequired()
64     }
65 
66     private fun initListenerIfRequired() {
67         if (mRegisteredListeners.isEmpty()) {
68             return
69         }
70         if (mListenerAccessIsGranted) {
71             return
72         }
73         mListenerAccessIsGranted = true
74         setNotificationListenerAccessGranted(
75             LISTENER_COMPONENT, granted = true, TestApis.users().instrumented()
76         )
77     }
78 
79     private fun teardownListenerIfRequired() {
80         if (!mRegisteredListeners.isEmpty()) {
81             return
82         }
83         if (!mListenerAccessIsGranted) {
84             return
85         }
86         mListenerAccessIsGranted = false
87         setNotificationListenerAccessGranted(
88             LISTENER_COMPONENT, granted = false, TestApis.users().instrumented()
89         )
90     }
91 
92     internal fun onNotificationPosted(notification: Notification) {
93         for (notificationListener in mRegisteredListeners) {
94             notificationListener.onNotificationPosted(notification)
95         }
96     }
97 
98     /**
99      * See [NotificationManager.setNotificationListenerAccessGranted].
100      */
101     @SuppressLint("NewApi")
102     fun setNotificationListenerAccessGranted(
103         listener: ComponentReference, granted: Boolean, user: UserReference
104     ) {
105         if (!Versions.meetsMinimumSdkVersionRequirement(Build.VERSION_CODES.S)) {
106             val command = if (granted) "allow_listener" else "disallow_listener"
107             try {
108                 ShellCommand.builder("cmd notification")
109                     .addOperand(command)
110                     .addOperand(listener.componentName().flattenToShortString())
111                     .allowEmptyOutput(true)
112                     .validate { obj: String -> obj.isEmpty() }
113                     .execute()
114             } catch (e: AdbException) {
115                 throw NeneException(
116                     "Error setting notification listener access $granted", e
117                 )
118             }
119             return
120         }
121 
122         TestApis.permissions().withPermission(MANAGE_NOTIFICATION_LISTENERS).use {
123             TestApis.context().androidContextAsUser(user)
124                 .getSystemService(NotificationManager::class.java)!!
125                 .setNotificationListenerAccessGranted(
126                     listener.componentName(), granted,  /* userSet= */false
127                 )
128         }
129     }
130 
131     @JvmField
132     val LISTENER_COMPONENT: ComponentReference = TestApis.packages().component(
133         ComponentName(
134             TestApis.context().instrumentedContext().packageName,
135             NeneNotificationListenerService::class.java.canonicalName
136         )
137     )
138 }
139