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 
17 package android.app.notification.legacy.cts;
18 
19 import static android.app.NotificationManager.INTERRUPTION_FILTER_ALARMS;
20 import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE;
21 import static android.service.notification.NotificationListenerService.INTERRUPTION_FILTER_PRIORITY;
22 
23 import static junit.framework.Assert.assertEquals;
24 import static junit.framework.Assert.assertNull;
25 import static junit.framework.TestCase.fail;
26 
27 import static org.junit.Assume.assumeFalse;
28 
29 import static java.lang.Thread.sleep;
30 
31 import android.app.ActivityManager;
32 import android.app.AutomaticZenRule;
33 import android.app.Instrumentation;
34 import android.app.NotificationManager;
35 import android.app.UiAutomation;
36 import android.content.ComponentName;
37 import android.content.Context;
38 import android.content.IntentFilter;
39 import android.net.Uri;
40 import android.os.ParcelFileDescriptor;
41 import android.service.notification.Condition;
42 import android.service.notification.ZenModeConfig;
43 import android.util.ArraySet;
44 import android.util.Log;
45 
46 import androidx.test.InstrumentationRegistry;
47 import androidx.test.runner.AndroidJUnit4;
48 
49 import junit.framework.Assert;
50 
51 import org.junit.After;
52 import org.junit.Before;
53 import org.junit.Test;
54 import org.junit.runner.RunWith;
55 
56 import java.io.FileInputStream;
57 import java.io.IOException;
58 import java.io.InputStream;
59 
60 @RunWith(AndroidJUnit4.class)
61 public class ConditionProviderServiceTest {
62     private static String TAG = "CpsTest";
63 
64     private NotificationManager mNm;
65     private ActivityManager mActivityManager;
66     private Context mContext;
67     private ZenModeBroadcastReceiver mModeReceiver;
68     private IntentFilter mModeFilter;
69     private ArraySet<String> ids = new ArraySet<>();
70 
71     @Before
setUp()72     public void setUp() throws Exception {
73         mContext = InstrumentationRegistry.getContext();
74         mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
75         assumeFalse(mActivityManager.isLowRamDevice());
76 
77         toggleNotificationPolicyAccess(mContext.getPackageName(),
78                 InstrumentationRegistry.getInstrumentation(), true);
79         LegacyConditionProviderService.requestRebind(LegacyConditionProviderService.getId());
80         SecondaryConditionProviderService.requestRebind(SecondaryConditionProviderService.getId());
81         mNm = (NotificationManager) mContext.getSystemService(
82                 Context.NOTIFICATION_SERVICE);
83         mNm.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL);
84         mModeReceiver = new ZenModeBroadcastReceiver();
85         mModeFilter = new IntentFilter();
86         mModeFilter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED);
87         mContext.registerReceiver(mModeReceiver, mModeFilter);
88     }
89 
90     @After
tearDown()91     public void tearDown() throws Exception {
92         mContext.unregisterReceiver(mModeReceiver);
93         if (mNm == null) {
94             // assumption in setUp is false, so mNm is not initialized
95             return;
96         }
97         try {
98             for (String id : ids) {
99                 if (id != null) {
100                     if (!mNm.removeAutomaticZenRule(id)) {
101                         throw new Exception("Could not remove rule " + id);
102                     }
103                     sleep(100);
104                     assertNull(mNm.getAutomaticZenRule(id));
105                 }
106             }
107         } finally {
108             toggleNotificationPolicyAccess(mContext.getPackageName(),
109                     InstrumentationRegistry.getInstrumentation(), false);
110             pollForConnection(LegacyConditionProviderService.class, false);
111             pollForConnection(SecondaryConditionProviderService.class, false);
112         }
113     }
114 
115     @Test
testUnboundCPSMaintainsCondition_addsNewRule()116     public void testUnboundCPSMaintainsCondition_addsNewRule() throws Exception {
117         if (mActivityManager.isLowRamDevice()) {
118             return;
119         }
120 
121         // make sure service get bound
122         pollForConnection(SecondaryConditionProviderService.class, true);
123 
124         final ComponentName cn = SecondaryConditionProviderService.getId();
125 
126         // add rule
127         mModeReceiver.reset();
128 
129         addRule(cn, INTERRUPTION_FILTER_ALARMS, true);
130         pollForSubscribe(SecondaryConditionProviderService.getInstance());
131 
132         mModeReceiver.waitFor(1/*Secondary only*/, 1000/*Limit is 1 second*/);
133         assertEquals(INTERRUPTION_FILTER_ALARMS, mNm.getCurrentInterruptionFilter());
134 
135         // unbind service
136         SecondaryConditionProviderService.getInstance().requestUnbind();
137 
138         // verify that DND state doesn't change
139         assertEquals(INTERRUPTION_FILTER_ALARMS, mNm.getCurrentInterruptionFilter());
140 
141         // add a new rule
142         addRule(cn, INTERRUPTION_FILTER_NONE, true);
143 
144         // verify that the unbound service maintains it's DND vote
145         assertEquals(INTERRUPTION_FILTER_ALARMS, mNm.getCurrentInterruptionFilter());
146     }
147 
148     @Test
testUnboundCPSMaintainsCondition_otherConditionChanges()149     public void testUnboundCPSMaintainsCondition_otherConditionChanges() throws Exception {
150         if (mActivityManager.isLowRamDevice()) {
151             return;
152         }
153 
154         // make sure both services get bound
155         pollForConnection(LegacyConditionProviderService.class, true);
156         pollForConnection(SecondaryConditionProviderService.class, true);
157 
158         // add rules for both
159         mModeReceiver.reset();
160 
161         addRule(LegacyConditionProviderService.getId(), INTERRUPTION_FILTER_PRIORITY, true);
162         pollForSubscribe(LegacyConditionProviderService.getInstance());
163 
164         addRule(SecondaryConditionProviderService.getId(), INTERRUPTION_FILTER_ALARMS, true);
165         pollForSubscribe(SecondaryConditionProviderService.getInstance());
166 
167         mModeReceiver.waitFor(2/*Legacy and Secondary*/, 1000/*Limit is 1 second*/);
168         assertEquals(INTERRUPTION_FILTER_ALARMS, mNm.getCurrentInterruptionFilter());
169 
170         // unbind one of the services
171         SecondaryConditionProviderService.getInstance().requestUnbind();
172 
173         // verify that DND state doesn't change
174         assertEquals(INTERRUPTION_FILTER_ALARMS, mNm.getCurrentInterruptionFilter());
175 
176         // trigger a change in the bound service's condition
177         ((LegacyConditionProviderService) LegacyConditionProviderService.getInstance())
178                 .toggleDND(false);
179         sleep(500);
180 
181         // verify that the unbound service maintains it's DND vote
182         assertEquals(INTERRUPTION_FILTER_ALARMS, mNm.getCurrentInterruptionFilter());
183     }
184 
185     @Test
testUnboundCPSMaintainsCondition_otherProviderRuleChanges()186     public void testUnboundCPSMaintainsCondition_otherProviderRuleChanges() throws Exception {
187         if (mActivityManager.isLowRamDevice()) {
188             return;
189         }
190 
191         // make sure both services get bound
192         pollForConnection(LegacyConditionProviderService.class, true);
193         pollForConnection(SecondaryConditionProviderService.class, true);
194 
195         // add rules for both
196         mModeReceiver.reset();
197 
198         addRule(LegacyConditionProviderService.getId(), INTERRUPTION_FILTER_PRIORITY, true);
199         pollForSubscribe(LegacyConditionProviderService.getInstance());
200 
201         addRule(SecondaryConditionProviderService.getId(), INTERRUPTION_FILTER_ALARMS, true);
202         pollForSubscribe(SecondaryConditionProviderService.getInstance());
203 
204         mModeReceiver.waitFor(2/*Legacy and Secondary*/, 1000/*Limit is 1 second*/);
205         assertEquals(INTERRUPTION_FILTER_ALARMS, mNm.getCurrentInterruptionFilter());
206 
207         // unbind one of the services
208         SecondaryConditionProviderService.getInstance().requestUnbind();
209 
210         // verify that DND state doesn't change
211         assertEquals(INTERRUPTION_FILTER_ALARMS, mNm.getCurrentInterruptionFilter());
212 
213         // trigger a change in the bound service's rule
214         addRule(SecondaryConditionProviderService.getId(), INTERRUPTION_FILTER_PRIORITY, false);
215 
216         // verify that the unbound service maintains it's DND vote
217         assertEquals(INTERRUPTION_FILTER_ALARMS, mNm.getCurrentInterruptionFilter());
218     }
219 
220     @Test
testRequestRebindWhenLostAccess()221     public void testRequestRebindWhenLostAccess() throws Exception {
222         if (mActivityManager.isLowRamDevice()) {
223             return;
224         }
225 
226         // make sure it gets bound
227         pollForConnection(LegacyConditionProviderService.class, true);
228 
229         // request unbind
230         LegacyConditionProviderService.getInstance().requestUnbind();
231 
232         // make sure it unbinds
233         pollForConnection(LegacyConditionProviderService.class, false);
234 
235         // lose dnd access
236         toggleNotificationPolicyAccess(mContext.getPackageName(),
237                 InstrumentationRegistry.getInstrumentation(), false);
238 
239         // try to rebind
240         LegacyConditionProviderService.requestRebind(LegacyConditionProviderService.getId());
241 
242         // make sure it isn't rebound
243         try {
244             pollForConnection(LegacyConditionProviderService.class, true);
245             fail("Service got rebound after permission lost");
246         } catch (Exception e) {
247             // pass
248         }
249     }
250 
251     @Test
testRequestRebindWhenStillHasAccess()252     public void testRequestRebindWhenStillHasAccess() throws Exception {
253         if (mActivityManager.isLowRamDevice()) {
254             return;
255         }
256 
257         // make sure it gets bound
258         pollForConnection(LegacyConditionProviderService.class, true);
259 
260         // request unbind
261         LegacyConditionProviderService.getInstance().requestUnbind();
262 
263         // make sure it unbinds
264         pollForConnection(LegacyConditionProviderService.class, false);
265 
266         // try to rebind
267         LegacyConditionProviderService.requestRebind(LegacyConditionProviderService.getId());
268 
269         // make sure it did rebind
270         try {
271             pollForConnection(LegacyConditionProviderService.class, true);
272         } catch (Exception e) {
273             fail("Service should've been able to rebind");
274         }
275     }
276 
277     @Test
testMethodsExistAndDoNotThrow()278     public void testMethodsExistAndDoNotThrow() throws Exception {
279         // behavior is covered in cts verifier
280 
281         if (mActivityManager.isLowRamDevice()) {
282             return;
283         }
284 
285         // make sure it gets bound
286         pollForConnection(LegacyConditionProviderService.class, true);
287 
288         // request unbind
289         LegacyConditionProviderService.getInstance().onConnected();
290         LegacyConditionProviderService.getInstance().onRequestConditions(
291                 Condition.FLAG_RELEVANT_NOW);
292         LegacyConditionProviderService.getInstance().onSubscribe(Uri.EMPTY);
293         LegacyConditionProviderService.getInstance().onUnsubscribe(Uri.EMPTY);
294 
295     }
296 
addRule(ComponentName cn, int filter, boolean enabled)297     private void addRule(ComponentName cn, int filter, boolean enabled) {
298         final Uri conditionId = new Uri.Builder()
299                 .scheme(Condition.SCHEME)
300                 .authority(ZenModeConfig.SYSTEM_AUTHORITY)
301                 .appendPath(cn.toString())
302                 .build();
303         String id = mNm.addAutomaticZenRule(new AutomaticZenRule("name",
304                 cn, conditionId, filter, enabled));
305         Log.d(TAG, "Created rule with id " + id);
306         ids.add(id);
307     }
308 
toggleNotificationPolicyAccess(String packageName, Instrumentation instrumentation, boolean on)309     private void toggleNotificationPolicyAccess(String packageName,
310             Instrumentation instrumentation, boolean on) throws IOException {
311 
312         String command = " cmd notification " + (on ? "allow_dnd " : "disallow_dnd ") + packageName;
313 
314         runCommand(command, instrumentation);
315 
316         NotificationManager nm = mContext.getSystemService(NotificationManager.class);
317         Assert.assertEquals("Notification Policy Access Grant is " +
318                         nm.isNotificationPolicyAccessGranted() + " not " + on, on,
319                 nm.isNotificationPolicyAccessGranted());
320     }
321 
runCommand(String command, Instrumentation instrumentation)322     private void runCommand(String command, Instrumentation instrumentation) throws IOException {
323         UiAutomation uiAutomation = instrumentation.getUiAutomation();
324         // Execute command
325         try (ParcelFileDescriptor fd = uiAutomation.executeShellCommand(command)) {
326             Assert.assertNotNull("Failed to execute shell command: " + command, fd);
327             // Wait for the command to finish by reading until EOF
328             try (InputStream in = new FileInputStream(fd.getFileDescriptor())) {
329                 byte[] buffer = new byte[4096];
330                 while (in.read(buffer) > 0) {}
331             } catch (IOException e) {
332                 throw new IOException("Could not read stdout of command: " + command, e);
333             }
334         } finally {
335             uiAutomation.destroy();
336         }
337     }
338 
pollForSubscribe(PollableConditionProviderService service)339     private void pollForSubscribe(PollableConditionProviderService service)  throws Exception {
340         int tries = 30;
341         int delayMs = 200;
342 
343         while (tries-- > 0 && !service.subscribed) {
344             try {
345                 sleep(delayMs);
346             } catch (InterruptedException e) {
347                 e.printStackTrace();
348             }
349         }
350 
351         if (!service.subscribed) {
352             Log.d(TAG, "not subscribed");
353             throw new Exception("Service never got onSubscribe()");
354         }
355     }
356 
pollForConnection(Class<? extends PollableConditionProviderService> service, boolean waitForConnection)357     private void pollForConnection(Class<? extends PollableConditionProviderService> service,
358             boolean waitForConnection) throws Exception {
359         int tries = 100;
360         int delayMs = 200;
361 
362         PollableConditionProviderService instance =
363                 (PollableConditionProviderService) service.getMethod("getInstance").invoke(null);
364 
365         while (tries-- > 0 && (waitForConnection ? instance == null : instance != null)) {
366             try {
367                 sleep(delayMs);
368             } catch (InterruptedException e) {
369                 e.printStackTrace();
370             }
371             instance = (PollableConditionProviderService) service.getMethod("getInstance")
372                     .invoke(null);
373         }
374 
375         if (waitForConnection && instance == null) {
376             Log.d(TAG, service.getName() + " not bound");
377             throw new Exception("CPS never bound");
378         } else if (!waitForConnection && instance != null) {
379             Log.d(TAG, service.getName() + " still bound");
380             throw new Exception("CPS still bound");
381         } else {
382             Log.d(TAG, service.getName() + " has a correct bind state");
383         }
384     }
385 }
386