1 /*
2  * Copyright (C) 2017 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 com.android.server;
18 
19 import static libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
20 import static libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
21 
22 import static org.junit.Assert.assertEquals;
23 import static org.mockito.Mockito.any;
24 import static org.mockito.Mockito.doReturn;
25 import static org.mockito.Mockito.mock;
26 import static org.mockito.Mockito.never;
27 import static org.mockito.Mockito.reset;
28 import static org.mockito.Mockito.timeout;
29 import static org.mockito.Mockito.times;
30 import static org.mockito.Mockito.verify;
31 import static org.mockito.Mockito.when;
32 
33 import android.compat.testing.PlatformCompatChangeRule;
34 import android.content.ContentResolver;
35 import android.content.Context;
36 import android.net.nsd.NsdManager;
37 import android.net.nsd.NsdServiceInfo;
38 import android.os.Build;
39 import android.os.Handler;
40 import android.os.HandlerThread;
41 import android.os.Looper;
42 import android.os.Message;
43 
44 import androidx.test.filters.SmallTest;
45 
46 import com.android.server.NsdService.DaemonConnection;
47 import com.android.server.NsdService.DaemonConnectionSupplier;
48 import com.android.server.NsdService.NativeCallbackReceiver;
49 import com.android.testutils.DevSdkIgnoreRule;
50 import com.android.testutils.DevSdkIgnoreRunner;
51 import com.android.testutils.HandlerUtils;
52 
53 import org.junit.After;
54 import org.junit.Before;
55 import org.junit.Rule;
56 import org.junit.Test;
57 import org.junit.rules.TestRule;
58 import org.junit.runner.RunWith;
59 import org.mockito.ArgumentCaptor;
60 import org.mockito.Mock;
61 import org.mockito.MockitoAnnotations;
62 import org.mockito.Spy;
63 
64 // TODOs:
65 //  - test client can send requests and receive replies
66 //  - test NSD_ON ENABLE/DISABLED listening
67 @RunWith(DevSdkIgnoreRunner.class)
68 @SmallTest
69 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
70 public class NsdServiceTest {
71 
72     static final int PROTOCOL = NsdManager.PROTOCOL_DNS_SD;
73     private static final long CLEANUP_DELAY_MS = 500;
74     private static final long TIMEOUT_MS = 500;
75 
76     @Rule
77     public TestRule compatChangeRule = new PlatformCompatChangeRule();
78     @Mock Context mContext;
79     @Mock ContentResolver mResolver;
80     @Mock NsdService.NsdSettings mSettings;
81     NativeCallbackReceiver mDaemonCallback;
82     @Spy DaemonConnection mDaemon = new DaemonConnection(mDaemonCallback);
83     HandlerThread mThread;
84     TestHandler mHandler;
85 
86     @Before
87     public void setUp() throws Exception {
88         MockitoAnnotations.initMocks(this);
89         mThread = new HandlerThread("mock-service-handler");
90         mThread.start();
91         doReturn(true).when(mDaemon).execute(any());
92         mHandler = new TestHandler(mThread.getLooper());
93         when(mContext.getContentResolver()).thenReturn(mResolver);
94     }
95 
96     @After
97     public void tearDown() throws Exception {
98         if (mThread != null) {
99             mThread.quit();
100             mThread = null;
101         }
102     }
103 
104     @Test
105     @DisableCompatChanges(NsdManager.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS)
106     public void testPreSClients() {
107         when(mSettings.isEnabled()).thenReturn(true);
108         NsdService service = makeService();
109 
110         // Pre S client connected, the daemon should be started.
111         NsdManager client1 = connectClient(service);
112         waitForIdle();
113         verify(mDaemon, times(1)).maybeStart();
114         verifyDaemonCommands("start-service");
115 
116         NsdManager client2 = connectClient(service);
117         waitForIdle();
118         verify(mDaemon, times(1)).maybeStart();
119 
120         client1.disconnect();
121         // Still 1 client remains, daemon shouldn't be stopped.
122         waitForIdle();
123         verify(mDaemon, never()).maybeStop();
124 
125         client2.disconnect();
126         // All clients are disconnected, the daemon should be stopped.
127         verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
128         verifyDaemonCommands("stop-service");
129     }
130 
131     @Test
132     @EnableCompatChanges(NsdManager.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS)
133     public void testNoDaemonStartedWhenClientsConnect() {
134         when(mSettings.isEnabled()).thenReturn(true);
135 
136         NsdService service = makeService();
137 
138         // Creating an NsdManager will not cause any cmds executed, which means
139         // no daemon is started.
140         NsdManager client1 = connectClient(service);
141         waitForIdle();
142         verify(mDaemon, never()).execute(any());
143 
144         // Creating another NsdManager will not cause any cmds executed.
145         NsdManager client2 = connectClient(service);
146         waitForIdle();
147         verify(mDaemon, never()).execute(any());
148 
149         // If there is no active request, try to clean up the daemon
150         // every time the client disconnects.
151         client1.disconnect();
152         verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
153         reset(mDaemon);
154         client2.disconnect();
155         verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
156 
157         client1.disconnect();
158         client2.disconnect();
159     }
160 
161     @Test
162     @EnableCompatChanges(NsdManager.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS)
163     public void testClientRequestsAreGCedAtDisconnection() {
164         when(mSettings.isEnabled()).thenReturn(true);
165 
166         NsdService service = makeService();
167         NsdManager client = connectClient(service);
168 
169         waitForIdle();
170         verify(mDaemon, never()).maybeStart();
171         verify(mDaemon, never()).execute(any());
172 
173         NsdServiceInfo request = new NsdServiceInfo("a_name", "a_type");
174         request.setPort(2201);
175 
176         // Client registration request
177         NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class);
178         client.registerService(request, PROTOCOL, listener1);
179         waitForIdle();
180         verify(mDaemon, times(1)).maybeStart();
181         verifyDaemonCommands("start-service", "register 2 a_name a_type 2201");
182 
183         // Client discovery request
184         NsdManager.DiscoveryListener listener2 = mock(NsdManager.DiscoveryListener.class);
185         client.discoverServices("a_type", PROTOCOL, listener2);
186         waitForIdle();
187         verify(mDaemon, times(1)).maybeStart();
188         verifyDaemonCommand("discover 3 a_type");
189 
190         // Client resolve request
191         NsdManager.ResolveListener listener3 = mock(NsdManager.ResolveListener.class);
192         client.resolveService(request, listener3);
193         waitForIdle();
194         verify(mDaemon, times(1)).maybeStart();
195         verifyDaemonCommand("resolve 4 a_name a_type local.");
196 
197         // Client disconnects, stop the daemon after CLEANUP_DELAY_MS.
198         client.disconnect();
199         verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
200         // checks that request are cleaned
201         verifyDaemonCommands("stop-register 2", "stop-discover 3",
202                 "stop-resolve 4", "stop-service");
203 
204         client.disconnect();
205     }
206 
207     @Test
208     @EnableCompatChanges(NsdManager.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS)
209     public void testCleanupDelayNoRequestActive() {
210         when(mSettings.isEnabled()).thenReturn(true);
211 
212         NsdService service = makeService();
213         NsdManager client = connectClient(service);
214 
215         NsdServiceInfo request = new NsdServiceInfo("a_name", "a_type");
216         request.setPort(2201);
217         NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class);
218         client.registerService(request, PROTOCOL, listener1);
219         waitForIdle();
220         verify(mDaemon, times(1)).maybeStart();
221         verifyDaemonCommands("start-service", "register 2 a_name a_type 2201");
222 
223         client.unregisterService(listener1);
224         verifyDaemonCommand("stop-register 2");
225 
226         verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
227         verifyDaemonCommand("stop-service");
228         reset(mDaemon);
229         client.disconnect();
230         // Client disconnects, after CLEANUP_DELAY_MS, maybeStop the daemon.
231         verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
232     }
233 
234     private void waitForIdle() {
235         HandlerUtils.waitForIdle(mHandler, TIMEOUT_MS);
236     }
237 
238     NsdService makeService() {
239         DaemonConnectionSupplier supplier = (callback) -> {
240             mDaemonCallback = callback;
241             return mDaemon;
242         };
243         NsdService service = new NsdService(mContext, mSettings,
244                 mHandler, supplier, CLEANUP_DELAY_MS);
245         verify(mDaemon, never()).execute(any(String.class));
246         return service;
247     }
248 
249     NsdManager connectClient(NsdService service) {
250         return new NsdManager(mContext, service);
251     }
252 
253     void verifyDelayMaybeStopDaemon(long cleanupDelayMs) {
254         waitForIdle();
255         // Stop daemon shouldn't be called immediately.
256         verify(mDaemon, never()).maybeStop();
257         // Clean up the daemon after CLEANUP_DELAY_MS.
258         verify(mDaemon, timeout(cleanupDelayMs + TIMEOUT_MS)).maybeStop();
259     }
260 
261     void verifyDaemonCommands(String... wants) {
262         verifyDaemonCommand(String.join(" ", wants), wants.length);
263     }
264 
265     void verifyDaemonCommand(String want) {
266         verifyDaemonCommand(want, 1);
267     }
268 
269     void verifyDaemonCommand(String want, int n) {
270         waitForIdle();
271         final ArgumentCaptor<Object> argumentsCaptor = ArgumentCaptor.forClass(Object.class);
272         verify(mDaemon, times(n)).execute(argumentsCaptor.capture());
273         String got = "";
274         for (Object o : argumentsCaptor.getAllValues()) {
275             got += o + " ";
276         }
277         assertEquals(want, got.trim());
278         // rearm deamon for next command verification
279         reset(mDaemon);
280         doReturn(true).when(mDaemon).execute(any());
281     }
282 
283     public static class TestHandler extends Handler {
284         public Message lastMessage;
285 
286         TestHandler(Looper looper) {
287             super(looper);
288         }
289 
290         @Override
291         public void handleMessage(Message msg) {
292             lastMessage = obtainMessage();
293             lastMessage.copyFrom(msg);
294         }
295     }
296 }
297