1 /*
2  * Copyright (C) 2018 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 android.app.stubs.shared;
17 
18 import android.os.ConditionVariable;
19 import android.service.notification.NotificationListenerService;
20 import android.service.notification.StatusBarNotification;
21 import android.util.Log;
22 
23 import java.util.ArrayList;
24 import java.util.Collection;
25 import java.util.HashMap;
26 import java.util.Map;
27 import java.util.concurrent.CountDownLatch;
28 import java.util.stream.Collectors;
29 
30 public class TestNotificationListener extends NotificationListenerService {
31     public static final String TAG = "TestNotificationListener";
32     public static final String PKG = "android.app.stubs";
33     private static final long CONNECTION_TIMEOUT_MS = 1000;
34 
35     private ArrayList<String> mTestPackages = new ArrayList<>();
36 
37     public ArrayList<StatusBarNotification> mPosted = new ArrayList<>();
38     public ArrayList<StatusBarNotification> mRemoved = new ArrayList<>();
39     public Map<String, Integer> mRemovedReasons = new HashMap<>();
40     public RankingMap mRankingMap;
41     public Map<String, Boolean> mIntercepted = new HashMap<>();
42 
43     private CountDownLatch mPostedLatch = null;
44     private CountDownLatch mRankingUpdateLatch = null;
45     private CountDownLatch mRemovedLatch = null;
46 
47     /**
48      * This controls whether there is a listener connected or not. Depending on the method, if the
49      * caller tries to use a listener after it has disconnected, NMS can throw a SecurityException.
50      *
51      * There is no race between onListenerConnected() and onListenerDisconnected() because they are
52      * called in the same thread. The value that getInstance() sees is guaranteed to be the value
53      * that was set by onListenerConnected() because of the happens-before established by the
54      * condition variable.
55      */
56     private static final ConditionVariable INSTANCE_AVAILABLE = new ConditionVariable(false);
57     private static TestNotificationListener sNotificationListenerInstance = null;
58     boolean isConnected;
59 
60     @Override
onCreate()61     public void onCreate() {
62         super.onCreate();
63         mTestPackages.add(PKG);
64     }
65 
66     @Override
onListenerConnected()67     public void onListenerConnected() {
68         Log.d(TAG, "onListenerConnected() called");
69         super.onListenerConnected();
70         sNotificationListenerInstance = this;
71         INSTANCE_AVAILABLE.open();
72         isConnected = true;
73     }
74 
75     @Override
onListenerDisconnected()76     public void onListenerDisconnected() {
77         Log.d(TAG, "onListenerDisconnected() called");
78         INSTANCE_AVAILABLE.close();
79         sNotificationListenerInstance = null;
80         isConnected = false;
81     }
82 
getInstance()83     public static TestNotificationListener getInstance() {
84         if (INSTANCE_AVAILABLE.block(CONNECTION_TIMEOUT_MS)) {
85             return sNotificationListenerInstance;
86         }
87         return null;
88     }
89 
resetData()90     public void resetData() {
91         Log.d(TAG, "resetData() called");
92         mPosted.clear();
93         mRemovedReasons.clear();
94         mRemoved.clear();
95         mIntercepted.clear();
96     }
97 
addTestPackage(String packageName)98     public void addTestPackage(String packageName) {
99         mTestPackages.add(packageName);
100     }
101 
removeTestPackage(String packageName)102     public void removeTestPackage(String packageName) {
103         mTestPackages.remove(packageName);
104     }
105 
106     @Override
onNotificationPosted(StatusBarNotification sbn)107     public void onNotificationPosted(StatusBarNotification sbn) {
108         if (sbn == null || !mTestPackages.contains(sbn.getPackageName())) {
109             Log.d(TAG, "onNotificationPosted: skipping handling sbn=" + sbn + " testPackages="
110                     + listToString(mTestPackages));
111             return;
112         } else {
113             Log.d(TAG, "onNotificationPosted: sbn=" + sbn + " testPackages=" + listToString(
114                     mTestPackages));
115         }
116         mPosted.add(sbn);
117         maybeUpdateLatch(mPostedLatch);
118     }
119 
120     @Override
onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap)121     public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
122         if (sbn == null || !mTestPackages.contains(sbn.getPackageName())) {
123             Log.d(TAG, "onNotificationPosted: skipping handling sbn=" + sbn + " testPackages="
124                     + listToString(mTestPackages));
125             return;
126         } else {
127             Log.d(TAG, "onNotificationPosted: sbn=" + sbn + " testPackages=" + listToString(
128                     mTestPackages));
129         }
130         mRankingMap = rankingMap;
131         updateInterceptedRecords(rankingMap);
132         mPosted.add(sbn);
133         maybeUpdateLatch(mPostedLatch);
134     }
135 
maybeUpdateLatch(CountDownLatch latch)136     public void maybeUpdateLatch(CountDownLatch latch) {
137         if (latch != null) {
138             latch.countDown();
139         }
140     }
141 
142     @Override
onNotificationRemoved(StatusBarNotification sbn)143     public void onNotificationRemoved(StatusBarNotification sbn) {
144         if (sbn == null || !mTestPackages.contains(sbn.getPackageName())) {
145             Log.d(TAG, "onNotificationRemoved: skipping handling sbn=" + sbn + " testPackages="
146                     + listToString(mTestPackages));
147             return;
148         } else {
149             Log.d(TAG, "onNotificationRemoved: sbn=" + sbn
150                     + " testPackages=" + listToString(mTestPackages));
151         }
152         mPosted.remove(sbn);
153         mRemovedReasons.put(sbn.getKey(), -1);
154         mRemoved.add(sbn);
155         maybeUpdateLatch(mRemovedLatch);
156     }
157 
158     @Override
onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, int reason)159     public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap,
160             int reason) {
161         if (sbn == null || !mTestPackages.contains(sbn.getPackageName())) {
162             Log.d(TAG, "onNotificationRemoved: skipping handling sbn=" + sbn + " testPackages="
163                     + listToString(mTestPackages));
164             return;
165         } else {
166             Log.d(TAG, "onNotificationRemoved: sbn=" + sbn + " reason=" + reason
167                     + " testPackages=" + listToString(mTestPackages));
168         }
169         mRankingMap = rankingMap;
170         updateInterceptedRecords(rankingMap);
171         mPosted.remove(sbn);
172         mRemovedReasons.put(sbn.getKey(), reason);
173         mRemoved.add(sbn);
174         maybeUpdateLatch(mRemovedLatch);
175     }
176 
177     @Override
onNotificationRankingUpdate(RankingMap rankingMap)178     public void onNotificationRankingUpdate(RankingMap rankingMap) {
179         Log.d(TAG, "onNotificationRankingUpdate() called rankingMap=[" + rankingMap + "]");
180         mRankingMap = rankingMap;
181         updateInterceptedRecords(rankingMap);
182     }
183 
184     // update the local cache of intercepted records based on the given ranking map; should be run
185     // every time the listener gets updated ranking map info
updateInterceptedRecords(RankingMap rankingMap)186     private void updateInterceptedRecords(RankingMap rankingMap) {
187         maybeUpdateLatch(mRankingUpdateLatch);
188         for (String key : rankingMap.getOrderedKeys()) {
189             Ranking rank = new Ranking();
190             if (rankingMap.getRanking(key, rank)) {
191                 // matchesInterruptionFilter is true if the notification can bypass and false if
192                 // blocked so the "is intercepted" boolean is the opposite of that.
193                 mIntercepted.put(key, !rank.matchesInterruptionFilter());
194             }
195         }
196     }
197 
setPostedCountDown(int countDownNumber)198     public CountDownLatch setPostedCountDown(int countDownNumber) {
199         mPostedLatch = new CountDownLatch(countDownNumber);
200         return mPostedLatch;
201     }
202 
setRankingUpdateCountDown(int countDownNumber)203     public CountDownLatch setRankingUpdateCountDown(int countDownNumber) {
204         mRankingUpdateLatch = new CountDownLatch(countDownNumber);
205         return mRankingUpdateLatch;
206     }
207 
setRemovedCountDown(int countDownNumber)208     public CountDownLatch setRemovedCountDown(int countDownNumber) {
209         mRemovedLatch = new CountDownLatch(countDownNumber);
210         return mRemovedLatch;
211     }
212 
213     @Override
toString()214     public String toString() {
215         return "TestNotificationListener{"
216                 + "mTestPackages=[" + listToString(mTestPackages)
217                 + "], mPosted=[" + listToString(mPosted)
218                 + ", mRemoved=[" + listToString(mRemovedReasons.values())
219                 + "]}";
220     }
221 
listToString(Collection<?> list)222     private String listToString(Collection<?> list) {
223         return list.stream().map(Object::toString).collect(Collectors.joining(","));
224     }
225 }
226