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